Merge "Unifying the two different state listeners" into ub-launcher3-master
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 8d62ab8..04fd59c 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -30,9 +30,16 @@
              loading full resolution screenshots. -->
     <dimen name="recents_fast_fling_velocity">600dp</dimen>
 
+    <!-- These velocities are in dp / s -->
     <dimen name="quickstep_fling_threshold_velocity">500dp</dimen>
     <dimen name="quickstep_fling_min_velocity">250dp</dimen>
 
+    <!-- These speeds are in dp / ms -->
+    <dimen name="motion_pause_detector_speed_very_slow">0.0285dp</dimen>
+    <dimen name="motion_pause_detector_speed_somewhat_fast">0.285dp</dimen>
+    <dimen name="motion_pause_detector_speed_fast">0.5dp</dimen>
+    <dimen name="motion_pause_detector_min_displacement">48dp</dimen>
+
     <!-- Launcher app transition -->
     <dimen name="content_trans_y">50dp</dimen>
     <dimen name="workspace_trans_y">50dp</dimen>
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index c3df9c7..d6a7f21 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -16,10 +16,12 @@
 package com.android.quickstep;
 
 import static android.view.View.TRANSLATION_Y;
+
 import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.FAST_OVERVIEW;
+import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS_SPRING;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
@@ -31,6 +33,7 @@
 import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_ROTATION;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.annotation.TargetApi;
@@ -44,6 +47,9 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.view.View;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
@@ -56,6 +62,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.TestProtocol;
 import com.android.launcher3.allapps.DiscoveryBounce;
+import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.SpringObjectAnimator;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
@@ -105,6 +112,8 @@
 
     void onSwipeUpComplete(T activity);
 
+    @NonNull HomeAnimationFactory prepareHomeUI(T activity);
+
     AnimationFactory prepareRecentsUI(T activity, boolean activityVisible,
             boolean animateActivity, Consumer<AnimatorPlaybackController> callback);
 
@@ -234,6 +243,32 @@
             DiscoveryBounce.showForOverviewIfNeeded(activity);
         }
 
+        @NonNull
+        @Override
+        public HomeAnimationFactory prepareHomeUI(Launcher activity) {
+            DeviceProfile dp = activity.getDeviceProfile();
+
+            return new HomeAnimationFactory() {
+                @NonNull
+                @Override
+                public RectF getWindowTargetRect() {
+                    int halfIconSize = dp.iconSizePx / 2;
+                    float targetCenterX = dp.availableWidthPx / 2;
+                    float targetCenterY = dp.availableHeightPx - dp.hotseatBarSizePx;
+                    return new RectF(targetCenterX - halfIconSize, targetCenterY - halfIconSize,
+                            targetCenterX + halfIconSize, targetCenterY + halfIconSize);
+                }
+
+                @NonNull
+                @Override
+                public Animator createActivityAnimationToHome() {
+                    long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx);
+                    return activity.getStateManager().createAnimationToNewWorkspace(
+                            NORMAL, accuracy).getTarget();
+                }
+            };
+        }
+
         @Override
         public AnimationFactory prepareRecentsUI(Launcher activity, boolean activityVisible,
                 boolean animateActivity, Consumer<AnimatorPlaybackController> callback) {
@@ -263,6 +298,9 @@
             }
 
             return new AnimationFactory() {
+                private Animator mShelfAnim;
+                private ShelfAnimState mShelfState;
+
                 @Override
                 public void createActivityController(long transitionLength,
                         @InteractionType int interactionType) {
@@ -274,6 +312,40 @@
                 public void onTransitionCancelled() {
                     activity.getStateManager().goToState(startState, false /* animate */);
                 }
+
+                @Override
+                public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator,
+                        long duration) {
+                    if (mShelfState == shelfState) {
+                        return;
+                    }
+                    mShelfState = shelfState;
+                    if (mShelfAnim != null) {
+                        mShelfAnim.cancel();
+                    }
+                    if (mShelfState == ShelfAnimState.CANCEL) {
+                        return;
+                    }
+                    float shelfHiddenProgress = BACKGROUND_APP.getVerticalProgress(activity);
+                    float shelfOverviewProgress = OVERVIEW.getVerticalProgress(activity);
+                    float shelfPeekingProgress = shelfHiddenProgress
+                            - (shelfHiddenProgress - shelfOverviewProgress) * 0.25f;
+                    float toProgress = mShelfState == ShelfAnimState.HIDE
+                            ? shelfHiddenProgress
+                            : mShelfState == ShelfAnimState.PEEK
+                                    ? shelfPeekingProgress
+                                    : shelfOverviewProgress;
+                    mShelfAnim = createShelfAnim(activity, toProgress);
+                    mShelfAnim.addListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            mShelfAnim = null;
+                        }
+                    });
+                    mShelfAnim.setInterpolator(interpolator);
+                    mShelfAnim.setDuration(duration);
+                    mShelfAnim.start();
+                }
             };
         }
 
@@ -295,13 +367,12 @@
             }
 
             AnimatorSet anim = new AnimatorSet();
-            if (!activity.getDeviceProfile().isVerticalBarLayout()) {
-                Animator shiftAnim = new SpringObjectAnimator<>(activity.getAllAppsController(),
-                        ALL_APPS_PROGRESS_SPRING, "allAppsSpringFromACH",
-                        activity.getAllAppsController().getShiftRange(),
+            if (!activity.getDeviceProfile().isVerticalBarLayout()
+                    && !FeatureFlags.SWIPE_HOME.get()) {
+                // Don't animate the shelf when SWIPE_HOME is true, because we update it atomically.
+                Animator shiftAnim = createShelfAnim(activity,
                         fromState.getVerticalProgress(activity),
                         endState.getVerticalProgress(activity));
-                shiftAnim.setInterpolator(LINEAR);
                 anim.play(shiftAnim);
             }
 
@@ -322,6 +393,14 @@
             callback.accept(controller);
         }
 
+        private Animator createShelfAnim(Launcher activity, float ... progressValues) {
+            Animator shiftAnim = new SpringObjectAnimator(activity.getAllAppsController(),
+                    ALL_APPS_PROGRESS_SPRING, "allAppsSpringFromACH",
+                    activity.getAllAppsController().getShiftRange(), progressValues);
+            shiftAnim.setInterpolator(LINEAR);
+            return shiftAnim;
+        }
+
         /**
          * Scale down recents from the center task being full screen to being in overview.
          */
@@ -512,6 +591,35 @@
             // TODO:
         }
 
+        @NonNull
+        @Override
+        public HomeAnimationFactory prepareHomeUI(RecentsActivity activity) {
+            RecentsView recentsView = activity.getOverviewPanel();
+
+            return new HomeAnimationFactory() {
+                @NonNull
+                @Override
+                public RectF getWindowTargetRect() {
+                    float centerX = recentsView.getPivotX();
+                    float centerY = recentsView.getPivotY();
+                    return new RectF(centerX, centerY, centerX, centerY);
+                }
+
+                @NonNull
+                @Override
+                public Animator createActivityAnimationToHome() {
+                    Animator anim = ObjectAnimator.ofFloat(recentsView, CONTENT_ALPHA, 0);
+                    anim.addListener(new AnimationSuccessListener() {
+                        @Override
+                        public void onAnimationSuccess(Animator animator) {
+                            recentsView.startHome();
+                        }
+                    });
+                    return anim;
+                }
+            };
+        }
+
         @Override
         public AnimationFactory prepareRecentsUI(RecentsActivity activity, boolean activityVisible,
                 boolean animateActivity, Consumer<AnimatorPlaybackController> callback) {
@@ -524,12 +632,12 @@
 
             return new AnimationFactory() {
 
-                boolean isAnimatingHome = false;
+                boolean isAnimatingToRecents = false;
 
                 @Override
                 public void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) {
-                    isAnimatingHome = targets != null && targets.isAnimatingHome();
-                    if (!isAnimatingHome) {
+                    isAnimatingToRecents = targets != null && targets.isAnimatingHome();
+                    if (!isAnimatingToRecents) {
                         rv.setContentAlpha(1);
                     }
                     createActivityController(getSwipeUpDestinationAndLength(
@@ -539,7 +647,7 @@
 
                 @Override
                 public void createActivityController(long transitionLength, int interactionType) {
-                    if (!isAnimatingHome) {
+                    if (!isAnimatingToRecents) {
                         return;
                     }
 
@@ -667,10 +775,30 @@
 
     interface AnimationFactory {
 
+        enum ShelfAnimState {
+            HIDE(true), PEEK(true), OVERVIEW(false), CANCEL(false);
+
+            ShelfAnimState(boolean shouldPreformHaptic) {
+                this.shouldPreformHaptic = shouldPreformHaptic;
+            }
+
+            public final boolean shouldPreformHaptic;
+        }
+
         default void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) { }
 
         void createActivityController(long transitionLength, @InteractionType int interactionType);
 
         default void onTransitionCancelled() { }
+
+        default void setShelfState(ShelfAnimState animState, Interpolator interpolator,
+                long duration) { }
+    }
+
+    interface HomeAnimationFactory {
+
+        @NonNull RectF getWindowTargetRect();
+
+        @NonNull Animator createActivityAnimationToHome();
     }
 }
diff --git a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
index cd71f3d..34d04b5 100644
--- a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
+++ b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
@@ -21,6 +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.launcher3.util.RaceConditionTracker.ENTER;
 import static com.android.launcher3.util.RaceConditionTracker.EXIT;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
@@ -47,8 +48,10 @@
 import android.view.WindowManager;
 
 import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.RaceConditionTracker;
 import com.android.launcher3.util.TraceHelper;
+import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.util.RemoteAnimationTargetSet;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.AssistDataReceiver;
@@ -99,6 +102,7 @@
     private Rect mStableInsets = new Rect();
 
     private VelocityTracker mVelocityTracker;
+    private MotionPauseDetector mMotionPauseDetector;
     private MotionEventQueue mEventQueue;
     private boolean mIsGoingToLauncher;
 
@@ -114,6 +118,7 @@
         mRecentsModel = recentsModel;
         mHomeIntent = homeIntent;
         mVelocityTracker = velocityTracker;
+        mMotionPauseDetector = new MotionPauseDetector(base);
         mActivityControlHelper = activityControl;
         mMainThreadExecutor = mainThreadExecutor;
         mBackgroundThreadChoreographer = backgroundThreadChoreographer;
@@ -192,6 +197,10 @@
                 if (mPassedInitialSlop && mInteractionHandler != null) {
                     // Move
                     dispatchMotion(ev, displacement - mStartDisplacement);
+
+                    if (FeatureFlags.SWIPE_HOME.get()) {
+                        mMotionPauseDetector.addPosition(displacement);
+                    }
                 }
                 break;
             }
@@ -250,6 +259,7 @@
         mRecentsModel.getTasks(null);
         mInteractionHandler = handler;
         handler.setGestureEndCallback(mEventQueue::reset);
+        mMotionPauseDetector.setOnMotionPauseListener(handler::onMotionPauseChanged);
 
         CountDownLatch drawWaitLock = new CountDownLatch(1);
         handler.setLauncherOnDrawCallback(() -> {
@@ -336,7 +346,7 @@
         if (mInteractionHandler != null) {
             final WindowTransformSwipeHandler handler = mInteractionHandler;
             mInteractionHandler = null;
-            mIsGoingToLauncher = handler.mIsGoingToRecents;
+            mIsGoingToLauncher = handler.mIsGoingToRecents || handler.mIsGoingToHome;
             mMainThreadExecutor.execute(handler::reset);
         }
     }
@@ -414,6 +424,7 @@
            mVelocityTracker.addMovement(ev);
            if (ev.getActionMasked() == ACTION_POINTER_UP) {
                mVelocityTracker.clear();
+               mMotionPauseDetector.clear();
            }
         }
     }
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 9bbe57a..da4a3de 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -15,17 +15,11 @@
  */
 package com.android.quickstep;
 
-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 static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
 import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL;
 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 static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
 
@@ -33,15 +27,9 @@
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
-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.Rect;
 import android.os.Build;
-import android.os.PatternMatcher;
 import android.os.SystemClock;
 import android.util.Log;
 import android.view.View;
@@ -56,21 +44,16 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
 import com.android.quickstep.ActivityControlHelper.AnimationFactory;
-import com.android.quickstep.ActivityControlHelper.FallbackActivityControllerHelper;
-import com.android.quickstep.ActivityControlHelper.LauncherActivityControllerHelper;
 import com.android.quickstep.util.ClipAnimationHelper;
-import com.android.quickstep.util.TransformedRect;
 import com.android.quickstep.util.RemoteAnimationTargetSet;
+import com.android.quickstep.util.TransformedRect;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.LatencyTrackerCompat;
-import com.android.systemui.shared.system.PackageManagerWrapper;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
 import com.android.systemui.shared.system.TransactionCompat;
 
-import java.util.ArrayList;
-
 /**
  * Helper class to handle various atomic commands for switching between Overview.
  */
@@ -85,100 +68,16 @@
     private final ActivityManagerWrapper mAM;
     private final RecentsModel mRecentsModel;
     private final MainThreadExecutor mMainThreadExecutor;
-    private final ComponentName mMyHomeComponent;
-
-    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;
-
-    public Intent overviewIntent;
-    public ComponentName overviewComponent;
-    private ActivityControlHelper mActivityControlHelper;
+    private final OverviewComponentObserver mOverviewComponentObserver;
 
     private long mLastToggleTime;
 
-    public OverviewCommandHelper(Context context) {
+    public OverviewCommandHelper(Context context, OverviewComponentObserver observer) {
         mContext = context;
         mAM = ActivityManagerWrapper.getInstance();
         mMainThreadExecutor = new MainThreadExecutor();
         mRecentsModel = RecentsModel.INSTANCE.get(mContext);
-
-        Intent myHomeIntent = new Intent(Intent.ACTION_MAIN)
-                .addCategory(Intent.CATEGORY_HOME)
-                .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(defaultHome);
-            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);
-    }
-
-    public void onDestroy() {
-        mContext.unregisterReceiver(mUserPreferenceChangeReceiver);
-
-        if (mUpdateRegisteredPackage != null) {
-            mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver);
-            mUpdateRegisteredPackage = null;
-        }
+        mOverviewComponentObserver = observer;
     }
 
     public void onOverviewToggle() {
@@ -200,10 +99,6 @@
                 UserEventDispatcher.newInstance(mContext).logActionTip(actionType, viewType));
     }
 
-    public ActivityControlHelper getActivityControlHelper() {
-        return mActivityControlHelper;
-    }
-
     private class ShowRecentsCommand extends RecentsActivityCommand {
 
         @Override
@@ -225,7 +120,7 @@
         private boolean mUserEventLogged;
 
         public RecentsActivityCommand() {
-            mHelper = getActivityControlHelper();
+            mHelper = mOverviewComponentObserver.getActivityControlHelper();
             mCreateTime = SystemClock.elapsedRealtime();
             mRunningTaskId = RecentsModel.getRunningTaskId();
 
@@ -242,8 +137,10 @@
                 // Start overview
                 if (!mHelper.switchToRecentsIfVisible(true)) {
                     mListener = mHelper.createActivityInitListener(this::onActivityReady);
-                    mListener.registerAndStartActivity(overviewIntent, this::createWindowAnimation,
-                            mContext, mMainThreadExecutor.getHandler(), RECENTS_LAUNCH_DURATION);
+                    mListener.registerAndStartActivity(
+                            mOverviewComponentObserver.getOverviewIntent(),
+                            this::createWindowAnimation, mContext, mMainThreadExecutor.getHandler(),
+                            RECENTS_LAUNCH_DURATION);
                 }
             }
         }
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
new file mode 100644
index 0000000..e119e53
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -0,0 +1,159 @@
+/*
+ * 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.content.Intent.ACTION_PACKAGE_ADDED;
+import static android.content.Intent.ACTION_PACKAGE_CHANGED;
+import static android.content.Intent.ACTION_PACKAGE_REMOVED;
+
+import static com.android.systemui.shared.system.PackageManagerWrapper
+        .ACTION_PREFERRED_ACTIVITY_CHANGED;
+
+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.os.PatternMatcher;
+
+import com.android.quickstep.ActivityControlHelper.FallbackActivityControllerHelper;
+import com.android.quickstep.ActivityControlHelper.LauncherActivityControllerHelper;
+import com.android.systemui.shared.system.PackageManagerWrapper;
+
+import java.util.ArrayList;
+
+/**
+ * Class to keep track of the current overview component based off user preferences and app updates
+ * and provide callers the relevant classes.
+ */
+public final class OverviewComponentObserver {
+    private final BroadcastReceiver mUserPreferenceChangeReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            updateOverviewTargets();
+        }
+    };
+    private final BroadcastReceiver mOtherHomeAppUpdateReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            updateOverviewTargets();
+        }
+    };
+    private final Context mContext;
+    private final ComponentName mMyHomeComponent;
+    private String mUpdateRegisteredPackage;
+    private ActivityControlHelper mActivityControlHelper;
+    private Intent mOverviewIntent;
+
+    public OverviewComponentObserver(Context context) {
+        mContext = context;
+
+        Intent myHomeIntent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME)
+                .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));
+        updateOverviewTargets();
+    }
+
+    /**
+     * Update overview intent and {@link ActivityControlHelper} based off the current launcher home
+     * component.
+     */
+    private void updateOverviewTargets() {
+        ComponentName defaultHome = PackageManagerWrapper.getInstance()
+                .getHomeActivities(new ArrayList<>());
+
+        final String overviewIntentCategory;
+        ComponentName overviewComponent;
+        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(defaultHome);
+            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);
+            }
+        }
+
+        mOverviewIntent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(overviewIntentCategory)
+                .setComponent(overviewComponent)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    }
+
+    /**
+     * Clean up any registered receivers.
+     */
+    public void onDestroy() {
+        mContext.unregisterReceiver(mUserPreferenceChangeReceiver);
+
+        if (mUpdateRegisteredPackage != null) {
+            mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver);
+            mUpdateRegisteredPackage = null;
+        }
+    }
+
+    /**
+     * Get the current intent for going to the overview activity.
+     *
+     * @return the overview intent
+     */
+    public Intent getOverviewIntent() {
+        return mOverviewIntent;
+    }
+
+    /**
+     * Get the current activity control helper for managing interactions to the overview activity.
+     *
+     * @return the current activity control helper
+     */
+    public ActivityControlHelper getActivityControlHelper() {
+        return mActivityControlHelper;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TouchConsumer.java b/quickstep/src/com/android/quickstep/TouchConsumer.java
index 225d29b..057a2ee 100644
--- a/quickstep/src/com/android/quickstep/TouchConsumer.java
+++ b/quickstep/src/com/android/quickstep/TouchConsumer.java
@@ -20,12 +20,12 @@
 import android.view.Choreographer;
 import android.view.MotionEvent;
 
+import androidx.annotation.IntDef;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.function.Consumer;
 
-import androidx.annotation.IntDef;
-
 @TargetApi(Build.VERSION_CODES.O)
 @FunctionalInterface
 public interface TouchConsumer extends Consumer<MotionEvent> {
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 8b6867f..164c9bd 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -183,6 +183,7 @@
     private MainThreadExecutor mMainThreadExecutor;
     private ISystemUiProxy mISystemUiProxy;
     private OverviewCommandHelper mOverviewCommandHelper;
+    private OverviewComponentObserver mOverviewComponentObserver;
     private OverviewInteractionState mOverviewInteractionState;
     private OverviewCallbacks mOverviewCallbacks;
     private TaskOverlayFactory mTaskOverlayFactory;
@@ -198,7 +199,8 @@
         mAM = ActivityManagerWrapper.getInstance();
         mRecentsModel = RecentsModel.INSTANCE.get(this);
         mMainThreadExecutor = new MainThreadExecutor();
-        mOverviewCommandHelper = new OverviewCommandHelper(this);
+        mOverviewComponentObserver = new OverviewComponentObserver(this);
+        mOverviewCommandHelper = new OverviewCommandHelper(this, mOverviewComponentObserver);
         mMainThreadChoreographer = Choreographer.getInstance();
         mEventQueue = new MotionEventQueue(mMainThreadChoreographer, TouchConsumer.NO_OP);
         mOverviewInteractionState = OverviewInteractionState.INSTANCE.get(this);
@@ -218,7 +220,7 @@
     @Override
     public void onDestroy() {
         mInputConsumer.unregisterInputConsumer();
-        mOverviewCommandHelper.onDestroy();
+        mOverviewComponentObserver.onDestroy();
         sConnected = false;
         super.onDestroy();
     }
@@ -250,21 +252,22 @@
         if (runningTaskInfo == null && !forceToLauncher) {
             return TouchConsumer.NO_OP;
         } else if (forceToLauncher ||
-                mOverviewCommandHelper.getActivityControlHelper().isResumed()) {
+                mOverviewComponentObserver.getActivityControlHelper().isResumed()) {
             return OverviewTouchConsumer.newInstance(
-                    mOverviewCommandHelper.getActivityControlHelper(), false, mTouchInteractionLog);
+                    mOverviewComponentObserver.getActivityControlHelper(), false,
+                    mTouchInteractionLog);
         } else if (ENABLE_QUICKSTEP_LIVE_TILE.get() &&
-                mOverviewCommandHelper.getActivityControlHelper().isInLiveTileMode()) {
+                mOverviewComponentObserver.getActivityControlHelper().isInLiveTileMode()) {
             return OverviewTouchConsumer.newInstance(
-                    mOverviewCommandHelper.getActivityControlHelper(), false, mTouchInteractionLog,
-                    false /* waitForWindowAvailable */);
+                    mOverviewComponentObserver.getActivityControlHelper(), false,
+                    mTouchInteractionLog, false /* waitForWindowAvailable */);
         } else {
             if (tracker == null) {
                 tracker = VelocityTracker.obtain();
             }
             return new OtherActivityTouchConsumer(this, runningTaskInfo, mRecentsModel,
-                    mOverviewCommandHelper.overviewIntent,
-                    mOverviewCommandHelper.getActivityControlHelper(), mMainThreadExecutor,
+                    mOverviewComponentObserver.getOverviewIntent(),
+                    mOverviewComponentObserver.getActivityControlHelper(), mMainThreadExecutor,
                     mBackgroundThreadChoreographer, downHitTarget, mOverviewCallbacks,
                     mTaskOverlayFactory, mInputConsumer, tracker, mTouchInteractionLog);
         }
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index 33c7c4d..d118e99 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -20,21 +20,32 @@
 import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
 import static com.android.launcher3.util.RaceConditionTracker.ENTER;
 import static com.android.launcher3.util.RaceConditionTracker.EXIT;
+import static com.android.launcher3.config.FeatureFlags.SWIPE_HOME;
+import static com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState.HIDE;
+import static com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState.PEEK;
 import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_FROM_APP_START_DURATION;
 import static com.android.quickstep.QuickScrubController.QUICK_SWITCH_FROM_APP_START_DURATION;
 import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL;
 import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB;
+import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.HOME;
+import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.LAST_TASK;
+import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.NEW_TASK;
+import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.RECENTS;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
@@ -42,6 +53,7 @@
 import android.graphics.Canvas;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
@@ -57,6 +69,7 @@
 import android.view.animation.Interpolator;
 
 import androidx.annotation.AnyThread;
+import androidx.annotation.NonNull;
 import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
 
@@ -80,6 +93,7 @@
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
 import com.android.quickstep.ActivityControlHelper.AnimationFactory;
+import com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState;
 import com.android.quickstep.ActivityControlHelper.LayoutListener;
 import com.android.quickstep.TouchConsumer.InteractionType;
 import com.android.quickstep.TouchInteractionService.OverviewTouchConsumer;
@@ -89,6 +103,7 @@
 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.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.LatencyTrackerCompat;
@@ -115,27 +130,28 @@
     private static final int STATE_APP_CONTROLLER_RECEIVED = 1 << 4;
 
     // Interaction finish states
-    private static final int STATE_SCALED_CONTROLLER_RECENTS = 1 << 5;
-    private static final int STATE_SCALED_CONTROLLER_LAST_TASK = 1 << 6;
+    private static final int STATE_SCALED_CONTROLLER_HOME = 1 << 5;
+    private static final int STATE_SCALED_CONTROLLER_RECENTS = 1 << 6;
+    private static final int STATE_SCALED_CONTROLLER_LAST_TASK = 1 << 7;
 
-    private static final int STATE_HANDLER_INVALIDATED = 1 << 7;
-    private static final int STATE_GESTURE_STARTED_QUICKSTEP = 1 << 8;
-    private static final int STATE_GESTURE_STARTED_QUICKSCRUB = 1 << 9;
-    private static final int STATE_GESTURE_CANCELLED = 1 << 10;
-    private static final int STATE_GESTURE_COMPLETED = 1 << 11;
+    private static final int STATE_HANDLER_INVALIDATED = 1 << 8;
+    private static final int STATE_GESTURE_STARTED_QUICKSTEP = 1 << 9;
+    private static final int STATE_GESTURE_STARTED_QUICKSCRUB = 1 << 10;
+    private static final int STATE_GESTURE_CANCELLED = 1 << 11;
+    private static final int STATE_GESTURE_COMPLETED = 1 << 12;
 
     // States for quick switch/scrub
-    private static final int STATE_CURRENT_TASK_FINISHED = 1 << 12;
-    private static final int STATE_QUICK_SCRUB_START = 1 << 13;
-    private static final int STATE_QUICK_SCRUB_END = 1 << 14;
+    private static final int STATE_CURRENT_TASK_FINISHED = 1 << 13;
+    private static final int STATE_QUICK_SCRUB_START = 1 << 14;
+    private static final int STATE_QUICK_SCRUB_END = 1 << 15;
 
-    private static final int STATE_CAPTURE_SCREENSHOT = 1 << 15;
-    private static final int STATE_SCREENSHOT_CAPTURED = 1 << 16;
-    private static final int STATE_SCREENSHOT_VIEW_SHOWN = 1 << 17;
+    private static final int STATE_CAPTURE_SCREENSHOT = 1 << 16;
+    private static final int STATE_SCREENSHOT_CAPTURED = 1 << 17;
+    private static final int STATE_SCREENSHOT_VIEW_SHOWN = 1 << 18;
 
-    private static final int STATE_RESUME_LAST_TASK = 1 << 18;
-    private static final int STATE_START_NEW_TASK = 1 << 19;
-    private static final int STATE_ASSIST_DATA_RECEIVED = 1 << 20;
+    private static final int STATE_RESUME_LAST_TASK = 1 << 19;
+    private static final int STATE_START_NEW_TASK = 1 << 20;
+    private static final int STATE_ASSIST_DATA_RECEIVED = 1 << 21;
 
 
     private static final int LAUNCHER_UI_STATES =
@@ -160,6 +176,7 @@
             "STATE_LAUNCHER_DRAWN",
             "STATE_ACTIVITY_MULTIPLIER_COMPLETE",
             "STATE_APP_CONTROLLER_RECEIVED",
+            "STATE_SCALED_CONTROLLER_HOME",
             "STATE_SCALED_CONTROLLER_RECENTS",
             "STATE_SCALED_CONTROLLER_LAST_TASK",
             "STATE_HANDLER_INVALIDATED",
@@ -178,6 +195,30 @@
             "STATE_ASSIST_DATA_RECEIVED",
     };
 
+    enum GestureEndTarget {
+        HOME(1, STATE_SCALED_CONTROLLER_HOME, true, ContainerType.WORKSPACE),
+
+        RECENTS(1, STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
+                | STATE_SCREENSHOT_VIEW_SHOWN, true, ContainerType.TASKSWITCHER),
+
+        NEW_TASK(0, STATE_START_NEW_TASK, false, ContainerType.APP),
+
+        LAST_TASK(0, STATE_SCALED_CONTROLLER_LAST_TASK, false, ContainerType.APP);
+
+        GestureEndTarget(float endShift, int endState, boolean isLauncher, int containerType) {
+            this.endShift = endShift;
+            this.endState = endState;
+            this.isLauncher = isLauncher;
+            this.containerType = containerType;
+        }
+
+        // 0 is app, 1 is overview
+        public final float endShift;
+        public final int endState;
+        public final boolean isLauncher;
+        public final int containerType;
+    }
+
     public static final long MAX_SWIPE_DURATION = 350;
     public static final long MIN_SWIPE_DURATION = 80;
     public static final long MIN_OVERSHOOT_DURATION = 120;
@@ -187,11 +228,15 @@
             Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW));
     private static final String SCREENSHOT_CAPTURED_EVT = "ScreenshotCaptured";
 
+    private static final long SHELF_ANIM_DURATION = 120;
+
     private final ClipAnimationHelper mClipAnimationHelper;
     private final ClipAnimationHelper.TransformParams mTransformParams;
 
     protected Runnable mGestureEndCallback;
     protected boolean mIsGoingToRecents;
+    protected boolean mIsGoingToHome;
+    private boolean mIsShelfPeeking;
     private DeviceProfile mDp;
     private int mTransitionDragLength;
 
@@ -325,6 +370,13 @@
                         | STATE_SCALED_CONTROLLER_RECENTS,
                 this::finishCurrentTransitionToRecents);
 
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_COMPLETED
+                        | STATE_SCALED_CONTROLLER_HOME | STATE_APP_CONTROLLER_RECEIVED
+                        | STATE_ACTIVITY_MULTIPLIER_COMPLETE,
+                this::finishCurrentTransitionToHome);
+        mStateCallback.addCallback(STATE_SCALED_CONTROLLER_HOME | STATE_CURRENT_TASK_FINISHED,
+                this::reset);
+
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
                         | STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_SCALED_CONTROLLER_RECENTS
                         | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED
@@ -426,7 +478,7 @@
             mSyncTransactionApplier = applier;
         });
         mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
-            if (!mBgLongSwipeMode) {
+            if (!mBgLongSwipeMode && !mIsGoingToHome) {
                 updateFinalShift();
             }
         });
@@ -598,7 +650,7 @@
         if (displacement > mTransitionDragLength && mTransitionDragLength > 0) {
             mCurrentShift.updateValue(1);
 
-            if (!mBgLongSwipeMode) {
+            if (!mBgLongSwipeMode && !FeatureFlags.SWIPE_HOME.get()) {
                 mBgLongSwipeMode = true;
                 executeOnUiThread(this::onLongSwipeEnabledUi);
             }
@@ -615,6 +667,23 @@
         }
     }
 
+    public void onMotionPauseChanged(boolean isPaused) {
+        setShelfState(isPaused ? PEEK : HIDE, FAST_OUT_SLOW_IN, SHELF_ANIM_DURATION);
+    }
+
+    public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator, long duration) {
+        if (mInteractionType == INTERACTION_NORMAL) {
+            executeOnUiThread(() -> {
+                mAnimationFactory.setShelfState(shelfState, interpolator, duration);
+                mIsShelfPeeking = shelfState == PEEK;
+                if (mRecentsView != null && shelfState.shouldPreformHaptic) {
+                    mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
+                            HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+                }
+            });
+        }
+    }
+
     /**
      * Called by {@link #mLayoutListener} when launcher layout changes
      */
@@ -676,7 +745,8 @@
         final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
         if (passed != mPassedOverviewThreshold) {
             mPassedOverviewThreshold = passed;
-            if (mInteractionType == INTERACTION_NORMAL && mRecentsView != null) {
+            if (mInteractionType == INTERACTION_NORMAL && mRecentsView != null
+                    && !SWIPE_HOME.get()) {
                 mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
                     HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
             }
@@ -702,6 +772,9 @@
     }
 
     private void updateLauncherTransitionProgress() {
+        if (mIsGoingToHome) {
+            return;
+        }
         float progress = mCurrentShift.value;
         mLauncherTransitionController.setPlayFraction(
                 progress <= mShiftAtGestureStart || mShiftAtGestureStart >= 1
@@ -816,7 +889,7 @@
         float velocityXPxPerMs = velocityX / 1000;
         long duration = MAX_SWIPE_DURATION;
         float currentShift = mCurrentShift.value;
-        final boolean goingToRecents;
+        final GestureEndTarget endTarget;
         float endShift;
         final float startShift;
         Interpolator interpolator = DEACCEL;
@@ -825,24 +898,40 @@
         boolean goingToNewTask = mRecentsView != null && nextPage != runningTaskIndex;
         final boolean reachedOverviewThreshold = currentShift >= MIN_PROGRESS_FOR_OVERVIEW;
         if (!isFling) {
-            goingToRecents = reachedOverviewThreshold && mGestureStarted;
-            endShift = goingToRecents ? 1 : 0;
+            if (SWIPE_HOME.get()) {
+                if (mIsShelfPeeking) {
+                    endTarget = RECENTS;
+                } else if (goingToNewTask) {
+                    endTarget = NEW_TASK;
+                } else {
+                    endTarget = currentShift < MIN_PROGRESS_FOR_OVERVIEW ? LAST_TASK : HOME;
+                }
+            } else {
+                endTarget = reachedOverviewThreshold && mGestureStarted ? RECENTS : LAST_TASK;
+            }
+            endShift = endTarget.endShift;
             long expectedDuration = Math.abs(Math.round((endShift - currentShift)
                     * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
             duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
             startShift = currentShift;
-            interpolator = goingToRecents ? OVERSHOOT_1_2 : DEACCEL;
+            interpolator = endTarget == RECENTS ? OVERSHOOT_1_2 : DEACCEL;
         } else {
-            // If user scrolled to a new task, only go to recents if they already passed
-            // the overview threshold. Otherwise, we'll snap to the new task and launch it.
-            goingToRecents = endVelocity < 0 && (!goingToNewTask || reachedOverviewThreshold);
-            endShift = goingToRecents ? 1 : 0;
+            if (SWIPE_HOME.get() && endVelocity < 0 && !mIsShelfPeeking) {
+                endTarget = HOME;
+            } else if (endVelocity < 0 && (!goingToNewTask || reachedOverviewThreshold)) {
+                // If user scrolled to a new task, only go to recents if they already passed
+                // the overview threshold. Otherwise, we'll snap to the new task and launch it.
+                endTarget = RECENTS;
+            } else {
+                endTarget = goingToNewTask ? NEW_TASK : LAST_TASK;
+            }
+            endShift = endTarget.endShift;
             startShift = Utilities.boundToRange(currentShift - velocityPxPerMs
                     * SINGLE_FRAME_MS / mTransitionDragLength, 0, 1);
             float minFlingVelocity = mContext.getResources()
                     .getDimension(R.dimen.quickstep_fling_min_velocity);
             if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) {
-                if (goingToRecents) {
+                if (endTarget == RECENTS) {
                     Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams(
                             startShift, endShift, endShift, velocityPxPerMs, mTransitionDragLength);
                     endShift = overshoot.end;
@@ -860,12 +949,19 @@
                 }
             }
         }
-        if (goingToRecents) {
+
+        if (endTarget == HOME) {
+            setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
+            duration = Math.max(MIN_OVERSHOOT_DURATION, duration);
+        } else if (endTarget == RECENTS) {
             mRecentsAnimationWrapper.enableTouchProxy();
-        } else if (goingToNewTask) {
+            if (SWIPE_HOME.get()) {
+                setShelfState(ShelfAnimState.OVERVIEW, interpolator, duration);
+            }
+        } else if (endTarget == NEW_TASK) {
             // We aren't goingToRecents, and user scrolled/flung to a new task; snap to the closest
             // task in that direction and launch it (in startNewTask()).
-            int taskToLaunch = runningTaskIndex + (nextPage > runningTaskIndex ? 1 : - 1);
+            int taskToLaunch = runningTaskIndex + (nextPage > runningTaskIndex ? 1 : -1);
             if (taskToLaunch >= mRecentsView.getTaskViewCount()) {
                 // Scrolled to Clear all button, snap back to current task and resume it.
                 mRecentsView.snapToPage(runningTaskIndex, Math.toIntExact(duration));
@@ -882,17 +978,16 @@
                 duration = Math.max(duration, durationX);
             }
         }
-
-        animateToProgress(startShift, endShift, duration, interpolator, goingToRecents,
-                goingToNewTask, velocityPxPerMs);
+        animateToProgress(startShift, endShift, duration, interpolator, endTarget, velocityPxPerMs);
     }
 
-    private void doLogGesture(boolean toLauncher) {
+    private void doLogGesture(GestureEndTarget endTarget) {
         DeviceProfile dp = mDp;
         if (dp == null) {
             // We probably never received an animation controller, skip logging.
             return;
         }
+        boolean toLauncher = endTarget.isLauncher;
         final int direction;
         if (dp.isVerticalBarLayout()) {
             direction = (dp.isSeascape() ^ toLauncher) ? Direction.LEFT : Direction.RIGHT;
@@ -900,60 +995,92 @@
             direction = toLauncher ? Direction.UP : Direction.DOWN;
         }
 
-        int dstContainerType = toLauncher ? ContainerType.TASKSWITCHER : ContainerType.APP;
         UserEventDispatcher.newInstance(mContext).logStateChangeAction(
                 mLogAction, direction,
                 ContainerType.NAVBAR, ContainerType.APP,
-                dstContainerType,
+                endTarget.containerType,
                 0);
     }
 
     /** Animates to the given progress, where 0 is the current app and 1 is overview. */
     private void animateToProgress(float start, float end, long duration, Interpolator interpolator,
-            boolean goingToRecents, boolean goingToNewTask, float velocityPxPerMs) {
+            GestureEndTarget target, float velocityPxPerMs) {
         mRecentsAnimationWrapper.runOnInit(() -> animateToProgressInternal(start, end, duration,
-                interpolator, goingToRecents, goingToNewTask, velocityPxPerMs));
+                interpolator, target, velocityPxPerMs));
     }
 
     private void animateToProgressInternal(float start, float end, long duration,
-            Interpolator interpolator, boolean goingToRecents, boolean goingToNewTask,
-            float velocityPxPerMs) {
-        mIsGoingToRecents = goingToRecents;
-        ObjectAnimator anim = mCurrentShift.animateToValue(start, end).setDuration(duration);
-        anim.setInterpolator(interpolator);
-        anim.addListener(new AnimationSuccessListener() {
+            Interpolator interpolator, GestureEndTarget target, float velocityPxPerMs) {
+        mIsGoingToHome = target == HOME;
+        mIsGoingToRecents = target == RECENTS;
+        ActivityControlHelper.HomeAnimationFactory homeAnimFactory;
+        Animator windowAnim;
+        if (mIsGoingToHome) {
+            if (mActivity != null) {
+                homeAnimFactory = mActivityControlHelper.prepareHomeUI(mActivity);
+            } else {
+                homeAnimFactory = new ActivityControlHelper.HomeAnimationFactory() {
+                    @NonNull
+                    @Override
+                    public RectF getWindowTargetRect() {
+                        RectF fallbackTarget = new RectF(mClipAnimationHelper.getTargetRect());
+                        Utilities.scaleRectFAboutCenter(fallbackTarget, 0.25f);
+                        return fallbackTarget;
+                    }
+
+                    @NonNull
+                    @Override
+                    public Animator createActivityAnimationToHome() {
+                        return new AnimatorSet();
+                    }
+                };
+                mStateCallback.addChangeHandler(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
+                        isPresent -> mRecentsView.startHome());
+            }
+            windowAnim = createWindowAnimationToHome(start, homeAnimFactory.getWindowTargetRect());
+            mLauncherTransitionController = null;
+        } else {
+            windowAnim = mCurrentShift.animateToValue(start, end);
+            homeAnimFactory = null;
+        }
+        windowAnim.setDuration(duration).setInterpolator(interpolator);
+        windowAnim.addListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationSuccess(Animator animator) {
-                int recentsState = STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
-                        | STATE_SCREENSHOT_VIEW_SHOWN;
-                setStateOnUiThread(mIsGoingToRecents
-                        ? recentsState
-                        : goingToNewTask
-                            ? STATE_START_NEW_TASK
-                            : STATE_SCALED_CONTROLLER_LAST_TASK);
+                setStateOnUiThread(target.endState);
             }
         });
-        anim.start();
+        windowAnim.start();
         long startMillis = SystemClock.uptimeMillis();
+        // Always play the entire launcher animation when going home, since it is separate from
+        // the animation that has been controlled thus far.
+        final float finalStart = mIsGoingToHome ? 0 : start;
         executeOnUiThread(() -> {
             // Animate the launcher components at the same time as the window, always on UI thread.
+            // Adjust start progress and duration in case we are on a different thread.
+            long elapsedMillis = SystemClock.uptimeMillis() - startMillis;
+            elapsedMillis = Utilities.boundToRange(elapsedMillis, 0, duration);
+            float elapsedProgress = (float) elapsedMillis / duration;
+            float adjustedStart = Utilities.mapRange(elapsedProgress, finalStart, end);
+            long adjustedDuration = duration - elapsedMillis;
+            // We want to use the same interpolator as the window, but need to adjust it to
+            // interpolate over the remaining progress (end - start).
+            TimeInterpolator adjustedInterpolator = Interpolators.mapToProgress(
+                    interpolator, adjustedStart, end);
+            if (homeAnimFactory != null) {
+                Animator homeAnim = homeAnimFactory.createActivityAnimationToHome();
+                homeAnim.setDuration(adjustedDuration).setInterpolator(adjustedInterpolator);
+                homeAnim.start();
+                mLauncherTransitionController = null;
+            }
             if (mLauncherTransitionController == null) {
                 return;
             }
-            if (start == end || duration <= 0) {
+            if (finalStart == end || duration <= 0) {
                 mLauncherTransitionController.dispatchSetInterpolator(t -> end);
                 mLauncherTransitionController.getAnimationPlayer().end();
             } else {
-                // Adjust start progress and duration in case we are on a different thread.
-                long elapsedMillis = SystemClock.uptimeMillis() - startMillis;
-                elapsedMillis = Utilities.boundToRange(elapsedMillis, 0, duration);
-                float elapsedProgress = (float) elapsedMillis / duration;
-                float adjustedStart = Utilities.mapRange(elapsedProgress, start, end);
-                long adjustedDuration = duration - elapsedMillis;
-                // We want to use the same interpolator as the window, but need to adjust it to
-                // interpolate over the remaining progress (end - start).
-                mLauncherTransitionController.dispatchSetInterpolator(Interpolators.mapToProgress(
-                        interpolator, adjustedStart, end));
+                mLauncherTransitionController.dispatchSetInterpolator(adjustedInterpolator);
                 mLauncherTransitionController.getAnimationPlayer().setDuration(adjustedDuration);
 
                 if (QUICKSTEP_SPRINGS.get()) {
@@ -965,10 +1092,49 @@
         });
     }
 
+    /**
+     * Creates an Animator that transforms the current app window into the home app.
+     * @param startProgress The progress of {@link #mCurrentShift} to start the window from.
+     * @param endTarget Where to animate the window towards.
+     */
+    private Animator createWindowAnimationToHome(float startProgress, RectF endTarget) {
+        final RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet;
+        RectF startRect = new RectF(mClipAnimationHelper.applyTransform(targetSet,
+                mTransformParams.setProgress(startProgress)));
+        RectF originalTarget = new RectF(mClipAnimationHelper.getTargetRect());
+        final RectF finalTarget = endTarget;
+
+        final RectFEvaluator rectFEvaluator = new RectFEvaluator();
+        final RectF targetRect = new RectF();
+        final RectF currentRect = new RectF();
+
+        ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+        anim.addUpdateListener(animation -> {
+            float progress = animation.getAnimatedFraction();
+            float interpolatedProgress = Interpolators.ACCEL_2.getInterpolation(progress);
+            // Initially go towards original target (task view in recents),
+            // but accelerate towards the final target.
+            // TODO: This is technically not correct. Instead, motion should continue at
+            // the released velocity but accelerate towards the target.
+            targetRect.set(rectFEvaluator.evaluate(interpolatedProgress,
+                    originalTarget, finalTarget));
+            currentRect.set(rectFEvaluator.evaluate(progress, startRect, targetRect));
+            float alpha = 1 - interpolatedProgress;
+            SyncRtSurfaceTransactionApplierCompat syncTransactionApplier
+                    = Looper.myLooper() == mMainThreadHandler.getLooper()
+                            ? mSyncTransactionApplier
+                            : null;
+            mTransformParams.setCurrentRectAndTargetAlpha(currentRect, alpha)
+                    .setSyncTransactionApplier(syncTransactionApplier);
+            mClipAnimationHelper.applyTransform(targetSet, mTransformParams);
+        });
+        return anim;
+    }
+
     @UiThread
     private void resumeLastTaskForQuickstep() {
         setStateOnUiThread(STATE_RESUME_LAST_TASK);
-        doLogGesture(false /* toLauncher */);
+        doLogGesture(LAST_TASK);
         reset();
     }
 
@@ -987,7 +1153,7 @@
                     mMainThreadHandler);
         });
         mTouchInteractionLog.finishRecentsAnimation(false);
-        doLogGesture(false /* toLauncher */);
+        doLogGesture(NEW_TASK);
     }
 
     public void reset() {
@@ -1007,10 +1173,6 @@
 
         mActivityInitListener.unregister();
         mTaskSnapshot = null;
-
-        if (mRecentsView != null) {
-            mRecentsView.setOnScrollChangeListener(null);
-        }
     }
 
     private void invalidateHandlerWithLauncher() {
@@ -1019,6 +1181,7 @@
         mActivityControlHelper.getAlphaProperty(mActivity).setValue(1);
 
         mRecentsView.setRunningTaskIconScaledDown(false);
+        mRecentsView.setOnScrollChangeListener(null);
         mQuickScrubController.cancelActiveQuickscrub();
     }
 
@@ -1101,6 +1264,15 @@
         mTouchInteractionLog.finishRecentsAnimation(true);
     }
 
+    private void finishCurrentTransitionToHome() {
+        synchronized (mRecentsAnimationWrapper) {
+            mRecentsAnimationWrapper.finish(true /* toRecents */,
+                    () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
+        }
+        mTouchInteractionLog.finishRecentsAnimation(true);
+        doLogGesture(HOME);
+    }
+
     private void setupLauncherUiAfterSwipeUpAnimation() {
         if (mLauncherTransitionController != null) {
             mLauncherTransitionController.getAnimationPlayer().end();
@@ -1114,7 +1286,7 @@
 
         RecentsModel.INSTANCE.get(mContext).onOverviewShown(false, TAG);
 
-        doLogGesture(true /* toLauncher */);
+        doLogGesture(RECENTS);
         reset();
     }
 
@@ -1132,8 +1304,7 @@
         long duration = FeatureFlags.QUICK_SWITCH.get()
                 ? QUICK_SWITCH_FROM_APP_START_DURATION
                 : QUICK_SCRUB_FROM_APP_START_DURATION;
-        animateToProgress(mCurrentShift.value, 1f, duration, LINEAR, true /* goingToRecents */,
-                false /* goingToNewTask */, 1f);
+        animateToProgress(mCurrentShift.value, 1f, duration, LINEAR, RECENTS, 1f);
     }
 
     private void onQuickScrubStartUi() {
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 261f45d..9679b81 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -39,7 +39,7 @@
     }
 
     @Override
-    protected void startHome() {
+    public void startHome() {
         mActivity.startHome();
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
index 31de683..c612b05 100644
--- a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
+++ b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
@@ -364,8 +364,8 @@
 
     public static class TransformParams {
         float progress;
-        float offsetX;
-        float offsetScale;
+        public float offsetX;
+        public float offsetScale;
         @Nullable RectF currentRect;
         float targetAlpha;
         boolean forLiveTile;
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
new file mode 100644
index 0000000..258e922
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -0,0 +1,141 @@
+/*
+ * 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.content.Context;
+import android.content.res.Resources;
+import android.os.SystemClock;
+import android.view.MotionEvent;
+
+import com.android.launcher3.R;
+
+/**
+ * Given positions along x- or y-axis, tracks velocity and acceleration and determines when there is
+ * a pause in motion.
+ */
+public class MotionPauseDetector {
+
+    // The percentage of the previous speed that determines whether this is a rapid deceleration.
+    // The bigger this number, the easier it is to trigger the first pause.
+    private static final float RAPID_DECELERATION_FACTOR = 0.6f;
+
+    private final float mSpeedVerySlow;
+    private final float mSpeedSomewhatFast;
+    private final float mSpeedFast;
+    private final float mMinDisplacementForPause;
+
+    private Long mPreviousTime = null;
+    private Float mPreviousPosition = null;
+    private Float mPreviousVelocity = null;
+
+    private Float mFirstPosition = null;
+
+    private OnMotionPauseListener mOnMotionPauseListener;
+    private boolean mIsPaused;
+    // Bias more for the first pause to make it feel extra responsive.
+    private boolean mHasEverBeenPaused;
+
+    public MotionPauseDetector(Context context) {
+        Resources res = context.getResources();
+        mSpeedVerySlow = res.getDimension(R.dimen.motion_pause_detector_speed_very_slow);
+        mSpeedSomewhatFast = res.getDimension(R.dimen.motion_pause_detector_speed_somewhat_fast);
+        mSpeedFast = res.getDimension(R.dimen.motion_pause_detector_speed_fast);
+        mMinDisplacementForPause = res.getDimension(R.dimen.motion_pause_detector_min_displacement);
+    }
+
+    /**
+     * Get callbacks for when motion pauses and resumes, including an
+     * immediate callback with the current pause state.
+     */
+    public void setOnMotionPauseListener(OnMotionPauseListener listener) {
+        mOnMotionPauseListener = listener;
+        if (mOnMotionPauseListener != null) {
+            mOnMotionPauseListener.onMotionPauseChanged(mIsPaused);
+        }
+    }
+
+    /**
+     * Computes velocity and acceleration to determine whether the motion is paused.
+     * @param position The x or y component of the motion being tracked.
+     *
+     * TODO: Use historical positions as well, e.g. {@link MotionEvent#getHistoricalY(int, int)}.
+     */
+    public void addPosition(float position) {
+        if (mFirstPosition == null) {
+            mFirstPosition = position;
+        }
+        long time = SystemClock.uptimeMillis();
+        if (mPreviousTime != null && mPreviousPosition != null) {
+            long changeInTime = Math.max(1, time - mPreviousTime);
+            float changeInPosition = position - mPreviousPosition;
+            float velocity = changeInPosition / changeInTime;
+            if (mPreviousVelocity != null) {
+                checkMotionPaused(velocity, mPreviousVelocity, Math.abs(position - mFirstPosition));
+            }
+            mPreviousVelocity = velocity;
+        }
+        mPreviousTime = time;
+        mPreviousPosition = position;
+    }
+
+    private void checkMotionPaused(float velocity, float prevVelocity, float totalDisplacement) {
+        float speed = Math.abs(velocity);
+        float previousSpeed = Math.abs(prevVelocity);
+        boolean isPaused;
+        if (mIsPaused) {
+            // Continue to be paused until moving at a fast speed.
+            isPaused = speed < mSpeedFast || previousSpeed < mSpeedFast;
+        } else {
+            if (velocity < 0 != prevVelocity < 0) {
+                // We're just changing directions, not necessarily stopping.
+                isPaused = false;
+            } else {
+                isPaused = speed < mSpeedVerySlow && previousSpeed < mSpeedVerySlow;
+                if (!isPaused && !mHasEverBeenPaused) {
+                    // We want to be more aggressive about detecting the first pause to ensure it
+                    // feels as responsive as possible; getting two very slow speeds back to back
+                    // takes too long, so also check for a rapid deceleration.
+                    boolean isRapidDeceleration = speed < previousSpeed * RAPID_DECELERATION_FACTOR;
+                    isPaused = isRapidDeceleration && speed < mSpeedSomewhatFast;
+                }
+            }
+        }
+        boolean passedMinDisplacement = totalDisplacement >= mMinDisplacementForPause;
+        isPaused &= passedMinDisplacement;
+        if (mIsPaused != isPaused) {
+            mIsPaused = isPaused;
+            if (mIsPaused) {
+                mHasEverBeenPaused = true;
+            }
+            if (mOnMotionPauseListener != null) {
+                mOnMotionPauseListener.onMotionPauseChanged(mIsPaused);
+            }
+        }
+    }
+
+    public void clear() {
+        mPreviousTime = null;
+        mPreviousPosition = null;
+        mPreviousVelocity = null;
+        mFirstPosition = null;
+        setOnMotionPauseListener(null);
+        mIsPaused = mHasEverBeenPaused = false;
+    }
+
+    public interface OnMotionPauseListener {
+        void onMotionPauseChanged(boolean isPaused);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index f8eced0..b0ca4d7 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -92,7 +92,7 @@
     }
 
     @Override
-    protected void startHome() {
+    public void startHome() {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             takeScreenshotAndFinishRecentsAnimation(true,
                     () -> mActivity.getStateManager().goToState(NORMAL));
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 5cbae65..840d2bd 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -719,7 +719,7 @@
         }
     }
 
-    protected abstract void startHome();
+    public abstract void startHome();
 
     public void reset() {
         setRunningTaskViewShowScreenshot(false);
diff --git a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
index d2b3bcc..fe05c4f 100644
--- a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
+++ b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep.views;
 
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
@@ -33,6 +34,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ScrimView;
 
@@ -135,6 +137,11 @@
         if (mProgress >= 1) {
             mRemainingScreenColor = 0;
             mShelfColor = 0;
+            if (FeatureFlags.SWIPE_HOME.get()
+                    && mLauncher.getStateManager().getState() == BACKGROUND_APP) {
+                // Show the shelf background when peeking during swipe up.
+                mShelfColor = setColorAlphaBound(mEndScrim, mMidAlpha);
+            }
         } else if (mProgress >= mMidProgress) {
             mRemainingScreenColor = 0;
 
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 9470635..a6b3a19 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.isObservedEventType;
+import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
 import static com.android.launcher3.touch.OverScroll.OVERSCROLL_DAMP_FACTOR;
 
 import android.animation.LayoutTransition;
@@ -25,6 +26,7 @@
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.TypedArray;
+import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.provider.Settings;
@@ -121,6 +123,8 @@
 
     protected boolean mIsPageInTransition = false;
 
+    protected float mSpringOverScrollX;
+
     protected boolean mWasInOverscroll = false;
 
     protected int mUnboundedScrollX;
@@ -349,6 +353,11 @@
 
         boolean isXBeforeFirstPage = mIsRtl ? (x > mMaxScrollX) : (x < 0);
         boolean isXAfterLastPage = mIsRtl ? (x < 0) : (x > mMaxScrollX);
+
+        if (!isXBeforeFirstPage && !isXAfterLastPage) {
+            mSpringOverScrollX = 0;
+        }
+
         if (isXBeforeFirstPage) {
             super.scrollTo(mIsRtl ? mMaxScrollX : 0, y);
             if (mAllowOverScroll) {
@@ -988,12 +997,35 @@
         }
     }
 
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        if (mScroller.isSpringing() && mSpringOverScrollX != 0) {
+            int saveCount = canvas.save();
+
+            canvas.translate(-mSpringOverScrollX, 0);
+            super.dispatchDraw(canvas);
+
+            canvas.restoreToCount(saveCount);
+        } else {
+            super.dispatchDraw(canvas);
+        }
+    }
+
     protected void dampedOverScroll(int amount) {
-        if (amount == 0) return;
+        mSpringOverScrollX = amount;
+        if (amount == 0) {
+            return;
+        }
 
         int overScrollAmount = OverScroll.dampedScroll(amount, getMeasuredWidth());
+        mSpringOverScrollX = overScrollAmount;
+        if (mScroller.isSpringing()) {
+            invalidate();
+            return;
+        }
+
         if (amount < 0) {
-            super.scrollTo(overScrollAmount, getScrollY());
+            super.scrollTo(amount, getScrollY());
         } else {
             super.scrollTo(mMaxScrollX + overScrollAmount, getScrollY());
         }
@@ -1001,6 +1033,12 @@
     }
 
     protected void overScroll(int amount) {
+        mSpringOverScrollX = amount;
+        if (mScroller.isSpringing()) {
+            invalidate();
+            return;
+        }
+
         if (amount == 0) return;
 
         if (mFreeScroll && !mScroller.isFinished()) {
@@ -1372,7 +1410,12 @@
         // interpolator at zero, ie. 5. We use 4 to make it a little slower.
         duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
 
-        return snapToPage(whichPage, delta, duration);
+        if (QUICKSTEP_SPRINGS.get()) {
+            return snapToPage(whichPage, delta, duration, false, null,
+                    velocity * Math.signum(newX - getUnboundedScrollX()), true);
+        } else {
+            return snapToPage(whichPage, delta, duration);
+        }
     }
 
     public boolean snapToPage(int whichPage) {
@@ -1397,15 +1440,15 @@
 
         int newX = getScrollForPage(whichPage);
         final int delta = newX - getUnboundedScrollX();
-        return snapToPage(whichPage, delta, duration, immediate, interpolator);
+        return snapToPage(whichPage, delta, duration, immediate, interpolator, 0, false);
     }
 
     protected boolean snapToPage(int whichPage, int delta, int duration) {
-        return snapToPage(whichPage, delta, duration, false, null);
+        return snapToPage(whichPage, delta, duration, false, null, 0, false);
     }
 
     protected boolean snapToPage(int whichPage, int delta, int duration, boolean immediate,
-            TimeInterpolator interpolator) {
+            TimeInterpolator interpolator, float velocity, boolean spring) {
         if (mFirstLayout) {
             setCurrentPage(whichPage);
             return false;
@@ -1441,7 +1484,11 @@
             mScroller.setInterpolator(mDefaultInterpolator);
         }
 
-        mScroller.startScroll(getUnboundedScrollX(), delta, duration);
+        if (spring && QUICKSTEP_SPRINGS.get()) {
+            mScroller.startScrollSpring(getUnboundedScrollX(), delta, duration, velocity);
+        } else {
+            mScroller.startScroll(getUnboundedScrollX(), delta, duration);
+        }
 
         updatePageIndicator();
 
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index eb26961..3438a26 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -996,7 +996,7 @@
 
     @Override
     protected void overScroll(int amount) {
-        boolean shouldScrollOverlay = mLauncherOverlay != null &&
+        boolean shouldScrollOverlay = mLauncherOverlay != null && !mScroller.isSpringing() &&
                 ((amount <= 0 && !mIsRtl) || (amount >= 0 && mIsRtl));
 
         boolean shouldZeroOverlay = mLauncherOverlay != null && mLastOverlayScroll != 0 &&
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index da99142..6ad69d7 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -106,7 +106,7 @@
 
     public static final ToggleableGlobalSettingsFlag SWIPE_HOME
             = new ToggleableGlobalSettingsFlag("SWIPE_HOME", false,
-            "[WIP] Swiping up on the nav bar goes home. Swipe and hold goes to recent apps.");
+            "Swiping up on the nav bar goes home. Swipe and hold goes to recent apps.");
 
     public static void initialize(Context context) {
         // Avoid the disk read for user builds
diff --git a/src/com/android/launcher3/util/OverScroller.java b/src/com/android/launcher3/util/OverScroller.java
index d697ece..fc8a138 100644
--- a/src/com/android/launcher3/util/OverScroller.java
+++ b/src/com/android/launcher3/util/OverScroller.java
@@ -26,6 +26,11 @@
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
 /**
  * Based on {@link android.widget.OverScroller} supporting only 1-d scrolling and with more
  * customization options.
@@ -196,6 +201,9 @@
 
         switch (mMode) {
             case SCROLL_MODE:
+                if (isSpringing()) {
+                    return true;
+                }
                 long time = AnimationUtils.currentAnimationTimeMillis();
                 // Any scroller can be used for time, since they were started
                 // together in scroll mode. We use X here.
@@ -254,6 +262,22 @@
     }
 
     /**
+     * Start scrolling using a spring by providing a starting point and the distance to travel.
+     *
+     * @param start Starting scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param delta Distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param duration Duration of the scroll in milliseconds.
+     * @param velocity The starting velocity for the spring in px per ms.
+     */
+    public void startScrollSpring(int start, int delta, int duration, float velocity) {
+        mMode = SCROLL_MODE;
+        mScroller.mState = mScroller.SPRING;
+        mScroller.startScroll(start, delta, duration, velocity);
+    }
+
+    /**
      * Call this when you want to 'spring back' into a valid coordinate range.
      *
      * @param start Starting X coordinate
@@ -354,6 +378,10 @@
         return (int) (time - mScroller.mStartTime);
     }
 
+    public boolean isSpringing() {
+        return mScroller.mState == SplineOverScroller.SPRING && !isFinished();
+    }
+
     static class SplineOverScroller {
         // Initial position
         private int mStart;
@@ -397,6 +425,8 @@
         // Current state of the animation.
         private int mState = SPLINE;
 
+        private SpringAnimation mSpring;
+
         // Constant gravity value, used in the deceleration phase.
         private static final float GRAVITY = 2000.0f;
 
@@ -417,6 +447,20 @@
         private static final int SPLINE = 0;
         private static final int CUBIC = 1;
         private static final int BALLISTIC = 2;
+        private static final int SPRING = 3;
+
+        private static final FloatPropertyCompat<SplineOverScroller> SPRING_PROPERTY =
+                new FloatPropertyCompat<SplineOverScroller>("splineOverScrollerSpring") {
+                    @Override
+                    public float getValue(SplineOverScroller scroller) {
+                        return scroller.mCurrentPosition;
+                    }
+
+                    @Override
+                    public void setValue(SplineOverScroller scroller, float value) {
+                        scroller.mCurrentPosition = (int) value;
+                    }
+                };
 
         static {
             float x_min = 0.0f;
@@ -465,6 +509,9 @@
         }
 
         void updateScroll(float q) {
+            if (mState == SPRING) {
+                return;
+            }
             mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
         }
 
@@ -495,6 +542,10 @@
         }
 
         void startScroll(int start, int distance, int duration) {
+            startScroll(start, distance, duration, 0);
+        }
+
+        void startScroll(int start, int distance, int duration, float velocity) {
             mFinished = false;
 
             mCurrentPosition = mStart = start;
@@ -503,12 +554,31 @@
             mStartTime = AnimationUtils.currentAnimationTimeMillis();
             mDuration = duration;
 
+            if (mState == SPRING) {
+                if (mSpring != null) {
+                    mSpring.cancel();
+                }
+                mSpring = new SpringAnimation(this, SPRING_PROPERTY);
+
+                mSpring.setSpring(new SpringForce(mFinal)
+                        .setStiffness(SpringForce.STIFFNESS_LOW)
+                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
+                mSpring.setStartVelocity(velocity);
+                mSpring.animateToFinalPosition(mFinal);
+                mSpring.addEndListener((animation, canceled, value, velocity1) -> {
+                    finish();
+                    mState = SPLINE;
+                    mSpring = null;
+                });
+            }
             // Unused
             mDeceleration = 0.0f;
             mVelocity = 0;
         }
 
         void finish() {
+            if (mSpring != null && mSpring.isRunning()) mSpring.cancel();
+
             mCurrentPosition = mFinal;
             // Not reset since WebView relies on this value for fast fling.
             // TODO: restore when WebView uses the fast fling implemented in this class.
@@ -518,6 +588,9 @@
 
         void setFinalPosition(int position) {
             mFinal = position;
+            if (mState == SPRING && mSpring != null) {
+                mSpring.animateToFinalPosition(mFinal);
+            }
             mSplineDistance = mFinal - mStart;
             mFinished = false;
         }
@@ -722,6 +795,10 @@
          * reached.
          */
         boolean update() {
+            if (mState == SPRING) {
+                return mFinished;
+            }
+
             final long time = AnimationUtils.currentAnimationTimeMillis();
             final long currentTime = time - mStartTime;