Swipe-up support for 3P Launcher

On swipe up, we start a rencets transition to the current launcher app. At the
end of the transition, if the user is going to recents, we start overview activity.

Bug: 137197916
Change-Id: Ie5ed848879ad965dcab2780a05d649e3be066568
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 332e0fa..5465480 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -23,6 +23,7 @@
     package="com.android.launcher3" >
 
     <uses-permission android:name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS" />
+    <uses-permission android:name="android.permission.VIBRATE" />
 
     <application
         android:backupAgent="com.android.launcher3.LauncherBackupAgent"
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
new file mode 100644
index 0000000..6e4a408
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2019 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 android.os.VibrationEffect.EFFECT_CLICK;
+import static android.os.VibrationEffect.createPredefined;
+
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.quickstep.TouchInteractionService.BACKGROUND_EXECUTOR;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.Utilities;
+import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.ClipAnimationHelper.TransformParams;
+
+/**
+ * Base class for swipe up handler with some utility methods
+ */
+@TargetApi(Build.VERSION_CODES.Q)
+public abstract class BaseSwipeUpHandler {
+
+    // Start resisting when swiping past this factor of mTransitionDragLength.
+    private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = 1.4f;
+    // This is how far down we can scale down, where 0f is full screen and 1f is recents.
+    private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = 1.8f;
+    private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
+
+    // The distance needed to drag to reach the task size in recents.
+    protected int mTransitionDragLength;
+    // How much further we can drag past recents, as a factor of mTransitionDragLength.
+    protected float mDragLengthFactor = 1;
+
+    protected final Context mContext;
+
+    protected final ClipAnimationHelper mClipAnimationHelper;
+    protected final TransformParams mTransformParams = new TransformParams();
+
+    private final Vibrator mVibrator;
+
+    protected BaseSwipeUpHandler(Context context) {
+        mContext = context;
+        mClipAnimationHelper = new ClipAnimationHelper(context);
+
+        mVibrator = context.getSystemService(Vibrator.class);
+    }
+
+    public void performHapticFeedback() {
+        if (!mVibrator.hasVibrator()) {
+            return;
+        }
+        if (Settings.System.getInt(
+                mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) == 0) {
+            return;
+        }
+
+        VibrationEffect effect = createPredefined(EFFECT_CLICK);
+        if (effect == null) {
+            return;
+        }
+        BACKGROUND_EXECUTOR.execute(() -> mVibrator.vibrate(effect));
+    }
+
+    protected float getShiftForDisplacement(float displacement) {
+        // We are moving in the negative x/y direction
+        displacement = -displacement;
+        if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) {
+            return mDragLengthFactor;
+        } else {
+            float translation = Math.max(displacement, 0);
+            float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
+            if (shift > DRAG_LENGTH_FACTOR_START_PULLBACK) {
+                float pullbackProgress = Utilities.getProgress(shift,
+                        DRAG_LENGTH_FACTOR_START_PULLBACK, mDragLengthFactor);
+                pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress);
+                shift = DRAG_LENGTH_FACTOR_START_PULLBACK + pullbackProgress
+                        * (DRAG_LENGTH_FACTOR_MAX_PULLBACK - DRAG_LENGTH_FACTOR_START_PULLBACK);
+            }
+            return shift;
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
index fc29a56..52e8ba2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
@@ -28,8 +28,10 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.app.ActivityOptions;
+import android.content.Intent;
 import android.content.res.Configuration;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.view.View;
 
@@ -42,7 +44,9 @@
 import com.android.quickstep.fallback.FallbackRecentsView;
 import com.android.quickstep.fallback.RecentsRootView;
 import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.ObjectWrapper;
 import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
 import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
 import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
@@ -54,6 +58,9 @@
  */
 public final class RecentsActivity extends BaseRecentsActivity {
 
+    public static final String EXTRA_THUMBNAIL = "thumbnailData";
+    public static final String EXTRA_TASK_ID = "taskID";
+
     private Handler mUiHandler = new Handler(Looper.getMainLooper());
     private RecentsRootView mRecentsRootView;
     private FallbackRecentsView mFallbackRecentsView;
@@ -79,6 +86,20 @@
     }
 
     @Override
+    protected void onNewIntent(Intent intent) {
+        int taskID = intent.getIntExtra(EXTRA_TASK_ID, 0);
+        IBinder thumbnail = intent.getExtras().getBinder(EXTRA_THUMBNAIL);
+        if (taskID != 0 && thumbnail instanceof ObjectWrapper) {
+            ThumbnailData thumbnailData = ((ObjectWrapper<ThumbnailData>) thumbnail).get();
+            mFallbackRecentsView.showCurrentTask(taskID);
+            mFallbackRecentsView.updateThumbnail(taskID, thumbnailData);
+        }
+        intent.removeExtra(EXTRA_TASK_ID);
+        intent.removeExtra(EXTRA_THUMBNAIL);
+        super.onNewIntent(intent);
+    }
+
+    @Override
     protected void onHandleConfigChanged() {
         super.onHandleConfigChanged();
         mRecentsRootView.setup();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
index ac7ba3f..da9c6cb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -43,7 +43,6 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
@@ -59,7 +58,6 @@
 import android.os.Looper;
 import android.os.SystemClock;
 import android.util.Log;
-import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnApplyWindowInsetsListener;
@@ -97,7 +95,7 @@
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.inputconsumers.InputConsumer;
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
-import com.android.quickstep.util.ClipAnimationHelper;
+import com.android.quickstep.util.ClipAnimationHelper.TargetAlphaProvider;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.RemoteAnimationTargetSet;
 import com.android.quickstep.util.SwipeAnimationTargetSet;
@@ -112,11 +110,10 @@
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
 import com.android.systemui.shared.system.WindowCallbacksCompat;
 
-import java.util.function.BiFunction;
 import java.util.function.Consumer;
 
 @TargetApi(Build.VERSION_CODES.O)
-public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
+public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> extends BaseSwipeUpHandler
         implements SwipeAnimationListener, OnApplyWindowInsetsListener {
     private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName();
 
@@ -220,30 +217,17 @@
     private static final long SHELF_ANIM_DURATION = 240;
     public static final long RECENTS_ATTACH_DURATION = 300;
 
-    // Start resisting when swiping past this factor of mTransitionDragLength.
-    private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = 1.4f;
-    // This is how far down we can scale down, where 0f is full screen and 1f is recents.
-    private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = 1.8f;
-    private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
-
     /**
      * Used as the page index for logging when we return to the last task at the end of the gesture.
      */
     private static final int LOG_NO_OP_PAGE_INDEX = -1;
 
-    private final ClipAnimationHelper mClipAnimationHelper;
-    private final ClipAnimationHelper.TransformParams mTransformParams;
-
     private Runnable mGestureEndCallback;
     private GestureEndTarget mGestureEndTarget;
     // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
     private RunningWindowAnim mRunningWindowAnim;
     private boolean mIsShelfPeeking;
     private DeviceProfile mDp;
-    // The distance needed to drag to reach the task size in recents.
-    private int mTransitionDragLength;
-    // How much further we can drag past recents, as a factor of mTransitionDragLength.
-    private float mDragLengthFactor = 1;
 
     // Shift in the range of [0, 1].
     // 0 => preview snapShot is completely visible, and hotseat is completely translated down
@@ -256,7 +240,6 @@
 
     private final Handler mMainThreadHandler = MAIN_THREAD_EXECUTOR.getHandler();
 
-    private final Context mContext;
     private final ActivityControlHelper<T> mActivityControlHelper;
     private final ActivityInitListener mActivityInitListener;
 
@@ -294,7 +277,7 @@
     public WindowTransformSwipeHandler(RunningTaskInfo runningTaskInfo, Context context,
             long touchTimeMs, ActivityControlHelper<T> controller, boolean continuingLastGesture,
             InputConsumerController inputConsumer) {
-        mContext = context;
+        super(context);
         mRunningTaskId = runningTaskInfo.id;
         mTouchTimeMs = touchTimeMs;
         mActivityControlHelper = controller;
@@ -303,8 +286,6 @@
         mContinuingLastGesture = continuingLastGesture;
         mRecentsAnimationWrapper = new RecentsAnimationWrapper(inputConsumer,
                 this::createNewInputProxyHandler);
-        mClipAnimationHelper = new ClipAnimationHelper(context);
-        mTransformParams = new ClipAnimationHelper.TransformParams();
 
         mMode = SysUINavigationMode.getMode(context);
         initStateCallbacks();
@@ -394,18 +375,6 @@
         }
     }
 
-    private long getFadeInDuration() {
-        if (mCurrentShift.getCurrentAnimation() != null) {
-            ObjectAnimator anim = mCurrentShift.getCurrentAnimation();
-            long theirDuration = anim.getDuration() - anim.getCurrentPlayTime();
-
-            // TODO: Find a better heuristic
-            return Math.min(MAX_SWIPE_DURATION, Math.max(theirDuration, MIN_SWIPE_DURATION));
-        } else {
-            return MAX_SWIPE_DURATION;
-        }
-    }
-
     public void initWhenReady() {
         mActivityInitListener.register();
     }
@@ -583,22 +552,7 @@
 
     @UiThread
     public void updateDisplacement(float displacement) {
-        // We are moving in the negative x/y direction
-        displacement = -displacement;
-        if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) {
-            mCurrentShift.updateValue(mDragLengthFactor);
-        } else {
-            float translation = Math.max(displacement, 0);
-            float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
-            if (shift > DRAG_LENGTH_FACTOR_START_PULLBACK) {
-                float pullbackProgress = Utilities.getProgress(shift,
-                        DRAG_LENGTH_FACTOR_START_PULLBACK, mDragLengthFactor);
-                pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress);
-                shift = DRAG_LENGTH_FACTOR_START_PULLBACK + pullbackProgress
-                        * (DRAG_LENGTH_FACTOR_MAX_PULLBACK - DRAG_LENGTH_FACTOR_START_PULLBACK);
-            }
-            mCurrentShift.updateValue(shift);
-        }
+        mCurrentShift.updateValue(getShiftForDisplacement(displacement));
     }
 
     public void onMotionPauseChanged(boolean isPaused) {
@@ -666,9 +620,8 @@
         if (mIsShelfPeeking != wasShelfPeeking) {
             maybeUpdateRecentsAttachedState();
         }
-        if (mRecentsView != null && shelfState.shouldPreformHaptic) {
-            mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
-                    HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+        if (shelfState.shouldPreformHaptic) {
+            performHapticFeedback();
         }
     }
 
@@ -722,9 +675,8 @@
         final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
         if (passed != mPassedOverviewThreshold) {
             mPassedOverviewThreshold = passed;
-            if (mRecentsView != null && mMode != Mode.NO_BUTTON) {
-                mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
-                    HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+            if (mMode != Mode.NO_BUTTON) {
+                performHapticFeedback();
             }
         }
 
@@ -1450,13 +1402,12 @@
         mGestureEndCallback = gestureEndCallback;
     }
 
-    private void setTargetAlphaProvider(
-            BiFunction<RemoteAnimationTargetCompat, Float, Float> provider) {
+    private void setTargetAlphaProvider(TargetAlphaProvider provider) {
         mClipAnimationHelper.setTaskAlphaCallback(provider);
         updateFinalShift();
     }
 
-    public static float getHiddenTargetAlpha(RemoteAnimationTargetCompat app, Float expectedAlpha) {
+    public static float getHiddenTargetAlpha(RemoteAnimationTargetCompat app, float expectedAlpha) {
         if (!isNotInRecents(app)) {
             return 0;
         }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
index d17bba4..cdd91b7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
@@ -23,22 +23,27 @@
 import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.INVALID_POINTER_ID;
 
-import static com.android.quickstep.WindowTransformSwipeHandler.MAX_SWIPE_DURATION;
+import static com.android.quickstep.RecentsActivity.EXTRA_TASK_ID;
+import static com.android.quickstep.RecentsActivity.EXTRA_THUMBNAIL;
 import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
-import static com.android.quickstep.WindowTransformSwipeHandler.MIN_SWIPE_DURATION;
+import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.HOME;
+import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.LAST_TASK;
+import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.RECENTS;
 import static com.android.quickstep.inputconsumers.OtherActivityInputConsumer.QUICKSTEP_TOUCH_SLOP_RATIO;
 import static com.android.quickstep.TouchInteractionService.startRecentsActivityAsync;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
+import android.animation.AnimatorSet;
 import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.os.Bundle;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
@@ -48,38 +53,50 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.R;
 import com.android.quickstep.ActivityControlHelper;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.BaseSwipeUpHandler;
 import com.android.quickstep.OverviewComponentObserver;
 import com.android.quickstep.SwipeSharedState;
-import com.android.quickstep.util.ClipAnimationHelper;
-import com.android.quickstep.util.ClipAnimationHelper.TransformParams;
+import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.util.NavBarPosition;
+import com.android.quickstep.util.ObjectWrapper;
 import com.android.quickstep.util.RecentsAnimationListenerSet;
 import com.android.quickstep.util.SwipeAnimationTargetSet;
 import com.android.quickstep.util.SwipeAnimationTargetSet.SwipeAnimationListener;
+import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputMonitorCompat;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
-public class FallbackNoButtonInputConsumer implements InputConsumer, SwipeAnimationListener {
+public class FallbackNoButtonInputConsumer extends BaseSwipeUpHandler
+        implements InputConsumer, SwipeAnimationListener {
 
-    private static final int STATE_NOT_FINISHED = 0;
-    private static final int STATE_FINISHED_TO_HOME = 1;
-    private static final int STATE_FINISHED_TO_APP = 2;
+    public enum GestureEndTarget {
+        HOME(3, 100, 1),
+        RECENTS(1, 300, 0),
+        LAST_TASK(0, 150, 1);
 
-    private static final float PROGRESS_TO_END_GESTURE = -2;
+        private final float mEndProgress;
+        private final long mDurationMultiplier;
+        private final float mLauncherAlpha;
+        GestureEndTarget(float endProgress, long durationMultiplier, float launcherAlpha) {
+            mEndProgress = endProgress;
+            mDurationMultiplier = durationMultiplier;
+            mLauncherAlpha = launcherAlpha;
+        }
+    }
 
     private final ActivityControlHelper mActivityControlHelper;
     private final InputMonitorCompat mInputMonitor;
-    private final Context mContext;
     private final NavBarPosition mNavBarPosition;
     private final SwipeSharedState mSwipeSharedState;
     private final OverviewComponentObserver mOverviewComponentObserver;
     private final int mRunningTaskId;
 
-    private final ClipAnimationHelper mClipAnimationHelper;
-    private final TransformParams mTransformParams = new TransformParams();
-    private final float mTransitionDragLength;
     private final DeviceProfile mDP;
+    private final MotionPauseDetector mMotionPauseDetector;
+    private final float mMotionPauseMinDisplacement;
+    private final Rect mTargetRect = new Rect();
 
     private final RectF mSwipeTouchRegion;
     private final boolean mDisableHorizontalSwipe;
@@ -87,6 +104,9 @@
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
 
+    private final AnimatedFloat mLauncherAlpha = new AnimatedFloat(this::onLauncherAlphaChanged);
+    private final AnimatedFloat mProgress = new AnimatedFloat(this::onProgressChanged);
+
     private int mActivePointerId = -1;
     // Slop used to determine when we say that the gesture has started.
     private boolean mPassedPilferInputSlop;
@@ -99,21 +119,26 @@
     // Might be displacement in X or Y, depending on the direction we are swiping from the nav bar.
     private float mStartDisplacement;
     private SwipeAnimationTargetSet mSwipeAnimationTargetSet;
-    private float mProgress;
 
-    private int mState = STATE_NOT_FINISHED;
+    private boolean mIsMotionPaused = false;
+    private GestureEndTarget mEndTarget;
 
     public FallbackNoButtonInputConsumer(Context context,
             ActivityControlHelper activityControlHelper, InputMonitorCompat inputMonitor,
             SwipeSharedState swipeSharedState, RectF swipeTouchRegion,
             OverviewComponentObserver overviewComponentObserver,
             boolean disableHorizontalSwipe, RunningTaskInfo runningTaskInfo) {
-        mContext = context;
+        super(context);
         mActivityControlHelper = activityControlHelper;
         mInputMonitor = inputMonitor;
         mOverviewComponentObserver = overviewComponentObserver;
         mRunningTaskId = runningTaskInfo.id;
 
+        mMotionPauseDetector = new MotionPauseDetector(context);
+        mMotionPauseMinDisplacement = context.getResources().getDimension(
+                R.dimen.motion_pause_detector_min_displacement_from_app);
+        mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseChanged);
+
         mSwipeSharedState = swipeSharedState;
         mSwipeTouchRegion = swipeTouchRegion;
         mDisableHorizontalSwipe = disableHorizontalSwipe;
@@ -124,13 +149,11 @@
         mTouchSlop = QUICKSTEP_TOUCH_SLOP_RATIO
                 * ViewConfiguration.get(context).getScaledTouchSlop();
 
-        mClipAnimationHelper = new ClipAnimationHelper(context);
-
         mDP = InvariantDeviceProfile.INSTANCE.get(context).getDeviceProfile(context).copy(context);
-        Rect tempRect = new Rect();
-        mTransitionDragLength = mActivityControlHelper.getSwipeUpDestinationAndLength(
-                mDP, context, tempRect);
-        mClipAnimationHelper.updateTargetRect(tempRect);
+        mLauncherAlpha.value = 1;
+
+        mClipAnimationHelper.setBaseAlphaCallback((t, a) -> mLauncherAlpha.value);
+        initTransitionTarget();
     }
 
     @Override
@@ -138,6 +161,19 @@
         return TYPE_FALLBACK_NO_BUTTON;
     }
 
+    private void onLauncherAlphaChanged() {
+        if (mSwipeAnimationTargetSet != null && mEndTarget == null) {
+            mClipAnimationHelper.applyTransform(mSwipeAnimationTargetSet, mTransformParams);
+        }
+    }
+
+    private void onMotionPauseChanged(boolean isPaused) {
+        mIsMotionPaused = isPaused;
+        mLauncherAlpha.animateToValue(mLauncherAlpha.value, isPaused ? 0 : 1)
+                .setDuration(150).start();
+        performHapticFeedback();
+    }
+
     @Override
     public void onMotionEvent(MotionEvent ev) {
         if (mVelocityTracker == null) {
@@ -147,6 +183,7 @@
         mVelocityTracker.addMovement(ev);
         if (ev.getActionMasked() == ACTION_POINTER_UP) {
             mVelocityTracker.clear();
+            mMotionPauseDetector.clear();
         }
 
         switch (ev.getActionMasked()) {
@@ -204,6 +241,9 @@
                     }
                 } else {
                     updateDisplacement(displacement - mStartDisplacement);
+                    mMotionPauseDetector.setDisallowPause(
+                            -displacement < mMotionPauseMinDisplacement);
+                    mMotionPauseDetector.addPosition(displacement, ev.getEventTime());
                 }
                 break;
             }
@@ -230,9 +270,11 @@
     }
 
     private void updateDisplacement(float displacement) {
-        mProgress = displacement / mTransitionDragLength;
-        mTransformParams.setProgress(mProgress);
+        mProgress.updateValue(getShiftForDisplacement(displacement));
+    }
 
+    private void onProgressChanged() {
+        mTransformParams.setProgress(mProgress.value);
         if (mSwipeAnimationTargetSet != null) {
             mClipAnimationHelper.applyTransform(mSwipeAnimationTargetSet, mTransformParams);
         }
@@ -251,7 +293,7 @@
      */
     private void finishTouchTracking(MotionEvent ev) {
         if (ev.getAction() == ACTION_CANCEL) {
-            mState = STATE_FINISHED_TO_APP;
+            mEndTarget = LAST_TASK;
         } else {
             mVelocityTracker.computeCurrentVelocity(1000,
                     ViewConfiguration.get(mContext).getScaledMaximumFlingVelocity());
@@ -264,53 +306,68 @@
                     .getDimension(R.dimen.quickstep_fling_threshold_velocity);
             boolean isFling = Math.abs(velocity) > flingThreshold;
 
-            boolean goingHome;
-            if (!isFling) {
-                goingHome = -mProgress >= MIN_PROGRESS_FOR_OVERVIEW;
+            if (isFling) {
+                mEndTarget = velocity < 0 ? HOME : LAST_TASK;
+            } else if (mIsMotionPaused) {
+                mEndTarget = RECENTS;
             } else {
-                goingHome = velocity < 0;
-            }
-
-            if (goingHome) {
-                mState = STATE_FINISHED_TO_HOME;
-            } else {
-                mState = STATE_FINISHED_TO_APP;
+                mEndTarget = mProgress.value >= MIN_PROGRESS_FOR_OVERVIEW ? HOME : LAST_TASK;
             }
         }
 
         if (mSwipeAnimationTargetSet != null) {
             finishAnimationTargetSet();
         }
+        mMotionPauseDetector.clear();
+    }
+
+    private void finishAnimationTargetSetAnimationComplete() {
+        switch (mEndTarget) {
+            case HOME:
+                mSwipeAnimationTargetSet.finishController(true, null, true);
+                break;
+            case LAST_TASK:
+                mSwipeAnimationTargetSet.finishController(false, null, false);
+                break;
+            case RECENTS: {
+                ThumbnailData thumbnail =
+                        mSwipeAnimationTargetSet.controller.screenshotTask(mRunningTaskId);
+                mSwipeAnimationTargetSet.controller.setCancelWithDeferredScreenshot(true);
+
+                ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
+                Bundle extras = new Bundle();
+                extras.putBinder(EXTRA_THUMBNAIL, new ObjectWrapper<>(thumbnail));
+                extras.putInt(EXTRA_TASK_ID, mRunningTaskId);
+
+                Intent intent = new Intent(mOverviewComponentObserver.getOverviewIntent())
+                        .putExtras(extras);
+                mContext.startActivity(intent, options.toBundle());
+                mSwipeAnimationTargetSet.controller.cleanupScreenshot();
+                break;
+            }
+        }
     }
 
     private void finishAnimationTargetSet() {
-        if (mState == STATE_FINISHED_TO_APP) {
-            mSwipeAnimationTargetSet.finishController(false, null, false);
-        } else {
-            if (mProgress < PROGRESS_TO_END_GESTURE) {
-                mSwipeAnimationTargetSet.finishController(true, null, true);
-            } else {
-                long duration = (long) (Math.min(mProgress - PROGRESS_TO_END_GESTURE, 1)
-                        * MAX_SWIPE_DURATION / Math.abs(PROGRESS_TO_END_GESTURE));
-                if (duration < 0) {
-                    duration = MIN_SWIPE_DURATION;
-                }
+        float endProgress = mEndTarget.mEndProgress;
 
-                ValueAnimator anim = ValueAnimator.ofFloat(mProgress, PROGRESS_TO_END_GESTURE);
-                anim.addUpdateListener(a -> {
-                    float p = (Float) anim.getAnimatedValue();
-                    mTransformParams.setProgress(p);
-                    mClipAnimationHelper.applyTransform(mSwipeAnimationTargetSet, mTransformParams);
-                });
-                anim.setDuration(duration);
-                anim.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        mSwipeAnimationTargetSet.finishController(true, null, true);
-                    }
-                });
-                anim.start();
-            }
+        if (mProgress.value != endProgress) {
+            AnimatorSet anim = new AnimatorSet();
+            anim.play(mLauncherAlpha.animateToValue(
+                    mLauncherAlpha.value, mEndTarget.mLauncherAlpha));
+            anim.play(mProgress.animateToValue(mProgress.value, endProgress));
+
+            anim.setDuration((long) (mEndTarget.mDurationMultiplier *
+                    Math.abs(endProgress - mProgress.value)));
+            anim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    finishAnimationTargetSetAnimationComplete();
+                }
+            });
+            anim.start();
+        } else {
+            finishAnimationTargetSetAnimationComplete();
         }
     }
 
@@ -321,21 +378,29 @@
         RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId);
 
         mDP.updateIsSeascape(mContext.getSystemService(WindowManager.class));
+        if (targetSet.homeContentInsets != null) {
+            mDP.updateInsets(targetSet.homeContentInsets);
+        }
+
         if (runningTaskTarget != null) {
             mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget);
         }
         mClipAnimationHelper.prepareAnimation(mDP, false /* isOpening */);
-
-        overviewStackBounds
-                .inset(-overviewStackBounds.width() / 5, -overviewStackBounds.height() / 5);
-        mClipAnimationHelper.updateTargetRect(overviewStackBounds);
+        initTransitionTarget();
         mClipAnimationHelper.applyTransform(mSwipeAnimationTargetSet, mTransformParams);
 
-        if (mState != STATE_NOT_FINISHED) {
+        if (mEndTarget != null) {
             finishAnimationTargetSet();
         }
     }
 
+    private void initTransitionTarget() {
+        mTransitionDragLength = mActivityControlHelper.getSwipeUpDestinationAndLength(
+                mDP, mContext, mTargetRect);
+        mDragLengthFactor = (float) mDP.heightPx / mTransitionDragLength;
+        mClipAnimationHelper.updateTargetRect(mTargetRect);
+    }
+
     @Override
     public void onRecentsAnimationCanceled() { }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
index de671e0..f0a2903 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ClipAnimationHelper.java
@@ -99,8 +99,8 @@
     // Whether to boost the opening animation target layers, or the closing
     private int mBoostModeTargetLayers = -1;
 
-    private BiFunction<RemoteAnimationTargetCompat, Float, Float> mTaskAlphaCallback =
-            (t, a1) -> a1;
+    private TargetAlphaProvider mTaskAlphaCallback = (t, a) -> a;
+    private TargetAlphaProvider mBaseAlphaCallback = (t, a) -> 1;
 
     public ClipAnimationHelper(Context context) {
         mWindowCornerRadius = getWindowCornerRadius(context.getResources());
@@ -187,12 +187,12 @@
             Rect crop = mTmpRect;
             crop.set(app.sourceContainerBounds);
             crop.offsetTo(0, 0);
-            float alpha = 1f;
+            float alpha;
             int layer = RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers);
             float cornerRadius = 0f;
             float scale = Math.max(params.currentRect.width(), mTargetRect.width()) / crop.width();
             if (app.mode == targetSet.targetMode) {
-                alpha = mTaskAlphaCallback.apply(app, params.targetAlpha);
+                alpha = mTaskAlphaCallback.getAlpha(app, params.targetAlpha);
                 if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
                     mTmpMatrix.setRectToRect(mSourceRect, params.currentRect, ScaleToFit.FILL);
                     mTmpMatrix.postTranslate(app.position.x, app.position.y);
@@ -214,9 +214,12 @@
                     // home target.
                     alpha = 1 - (progress * params.targetAlpha);
                 }
-            } else if (ENABLE_QUICKSTEP_LIVE_TILE.get() && launcherOnTop) {
-                crop = null;
-                layer = Integer.MAX_VALUE;
+            } else {
+                alpha = mBaseAlphaCallback.getAlpha(app, progress);
+                if (ENABLE_QUICKSTEP_LIVE_TILE.get() && launcherOnTop) {
+                    crop = null;
+                    layer = Integer.MAX_VALUE;
+                }
             }
 
             // Since radius is in Surface space, but we draw the rounded corners in screen space, we
@@ -247,11 +250,14 @@
         }
     }
 
-    public void setTaskAlphaCallback(
-            BiFunction<RemoteAnimationTargetCompat, Float, Float> callback) {
+    public void setTaskAlphaCallback(TargetAlphaProvider callback) {
         mTaskAlphaCallback = callback;
     }
 
+    public void setBaseAlphaCallback(TargetAlphaProvider callback) {
+        mBaseAlphaCallback = callback;
+    }
+
     public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv) {
         fromTaskThumbnailView(ttv, rv, null);
     }
@@ -357,6 +363,10 @@
         return mCurrentCornerRadius;
     }
 
+    public interface TargetAlphaProvider {
+        float getAlpha(RemoteAnimationTargetCompat target, float expectedAlpha);
+    }
+
     public static class TransformParams {
         float progress;
         public float offsetX;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ObjectWrapper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ObjectWrapper.java
new file mode 100644
index 0000000..abfe3ad
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ObjectWrapper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 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.os.Binder;
+import android.os.IBinder;
+
+/**
+ * Utility class to pass non-parcealable objects within same process using parcealable payload.
+ *
+ * It wraps the object in a binder as binders are singleton within a process
+ */
+public class ObjectWrapper<T> extends Binder {
+
+    private final T mObject;
+
+    public ObjectWrapper(T object) {
+        mObject = object;
+    }
+
+    public T get() {
+        return mObject;
+    }
+
+    public static IBinder wrap(Object obj) {
+        return new ObjectWrapper<>(obj);
+    }
+}