Merge "Animate icon scale when hitting recents button" into ub-launcher3-edmonton
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index c24850d..fe8d65e 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -20,7 +20,6 @@
 <manifest
     xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.launcher3">
-    <uses-sdk android:targetSdkVersion="23" android:minSdkVersion="21"/>
 
     <!--
     The manifest defines the common entries that should be present in any derivative of Launcher3.
diff --git a/build.gradle b/build.gradle
index 0030b8b..4ae6600 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,14 +1,16 @@
 buildscript {
     repositories {
         mavenCentral()
-        jcenter()
+        google()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:2.3.3'
-        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.0'
+        classpath 'com.android.tools.build:gradle:3.2.0-alpha12'
+        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.3'
     }
 }
 
+final String SUPPORT_LIBS_VERSION = '28.0.0-SNAPSHOT'
+
 apply plugin: 'com.android.application'
 apply plugin: 'com.google.protobuf'
 
@@ -23,6 +25,7 @@
         versionName "1.0"
 
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+        vectorDrawables.useSupportLibrary = true
     }
     buildTypes {
         debug {
@@ -30,18 +33,28 @@
         }
     }
 
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+    flavorDimensions "default"
+
     productFlavors {
         aosp {
+            dimension "default"
             applicationId 'com.android.launcher3'
             testApplicationId 'com.android.launcher3.tests'
         }
 
         l3go {
+            dimension "default"
             applicationId 'com.android.launcher3'
             testApplicationId 'com.android.launcher3.tests'
         }
 
         quickstep {
+            dimension "default"
             applicationId 'com.android.launcher3'
             testApplicationId 'com.android.launcher3.tests'
         }
@@ -98,27 +111,28 @@
 }
 
 repositories {
+    maven { url "../../../prebuilts/fullsdk-darwin/extras/android/m2repository" }
+    maven { url "../../../prebuilts/fullsdk-linux/extras/android/m2repository" }
     mavenCentral()
-    jcenter()
+    google()
 }
 
-final String SUPPORT_LIBS_VERSION = '28.0.0-SNAPSHOT'
 dependencies {
-    compile "com.android.support:support-v4:${SUPPORT_LIBS_VERSION}"
-    compile "com.android.support:support-dynamic-animation:${SUPPORT_LIBS_VERSION}"
-    compile "com.android.support:recyclerview-v7:${SUPPORT_LIBS_VERSION}"
-    compile 'com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-7'
+    implementation "com.android.support:support-v4:${SUPPORT_LIBS_VERSION}"
+    implementation "com.android.support:support-dynamic-animation:${SUPPORT_LIBS_VERSION}"
+    implementation "com.android.support:recyclerview-v7:${SUPPORT_LIBS_VERSION}"
+    implementation 'com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-7'
 
-    quickstepCompile fileTree(dir: "quickstep/libs", include: 'sysui_shared.jar')
+    quickstepImplementation fileTree(dir: "quickstep/libs", include: 'sysui_shared.jar')
 
-    testCompile 'junit:junit:4.12'
-    androidTestCompile "org.mockito:mockito-core:1.9.5"
-    androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
-    androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
-    androidTestCompile 'com.android.support.test:runner:1.0.0'
-    androidTestCompile 'com.android.support.test:rules:1.0.0'
-    androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
-    androidTestCompile "com.android.support:support-annotations:${SUPPORT_LIBS_VERSION}"
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation "org.mockito:mockito-core:1.9.5"
+    androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'
+    androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2'
+    androidTestImplementation 'com.android.support.test:runner:1.0.0'
+    androidTestImplementation 'com.android.support.test:rules:1.0.0'
+    androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
+    androidTestImplementation "com.android.support:support-annotations:${SUPPORT_LIBS_VERSION}"
 }
 
 protobuf {
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index f62d1d6..bab2cd7 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -21,8 +21,6 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.launcher3" >
 
-    <uses-sdk android:targetSdkVersion="23" android:minSdkVersion="21"/>
-
     <uses-permission android:name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS" />
     <application
         android:backupAgent="com.android.launcher3.LauncherBackupAgent"
diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar
index 19a9b17..0adc83a 100644
--- a/quickstep/libs/sysui_shared.jar
+++ b/quickstep/libs/sysui_shared.jar
Binary files differ
diff --git a/quickstep/res/layout/overview_clear_all_button.xml b/quickstep/res/layout/overview_clear_all_button.xml
index 8632f8b..c8f235f 100644
--- a/quickstep/res/layout/overview_clear_all_button.xml
+++ b/quickstep/res/layout/overview_clear_all_button.xml
@@ -3,12 +3,12 @@
 <TextView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/clear_all_button"
+    style="@android:style/Widget.DeviceDefault.Button.Borderless"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="end|top"
     android:fontFamily="sans-serif-medium"
     android:text="@string/recents_clear_all"
     android:textColor="?attr/workspaceTextColor"
-    android:background="?android:attr/selectableItemBackground"
     android:textSize="14sp"
 />
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
index c985354..29399142 100644
--- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
@@ -15,7 +15,9 @@
  */
 package com.android.launcher3;
 
+import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
 import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -32,23 +34,32 @@
 @TargetApi(Build.VERSION_CODES.P)
 public abstract class LauncherAnimationRunner implements RemoteAnimationRunnerCompat {
 
-    private static final int REFRESH_RATE_MS = 16;
-
     private final Handler mHandler;
+    private final boolean mStartAtFrontOfQueue;
     private AnimationResult mAnimationResult;
 
-    public LauncherAnimationRunner(Handler handler) {
+    /**
+     * @param startAtFrontOfQueue If true, the animation start will be posted at the front of the
+     *                            queue to minimize latency.
+     */
+    public LauncherAnimationRunner(Handler handler, boolean startAtFrontOfQueue) {
         mHandler = handler;
+        mStartAtFrontOfQueue = startAtFrontOfQueue;
     }
 
     @BinderThread
     @Override
     public void onAnimationStart(RemoteAnimationTargetCompat[] targetCompats, Runnable runnable) {
-        postAsyncCallback(mHandler, () -> {
+        Runnable r = () -> {
             finishExistingAnimation();
             mAnimationResult = new AnimationResult(runnable);
             onCreateAnimation(targetCompats, mAnimationResult);
-        });
+        };
+        if (mStartAtFrontOfQueue) {
+            postAtFrontOfQueueAsynchronously(mHandler, r);
+        } else {
+            postAsyncCallback(mHandler, r);
+        }
     }
 
     /**
@@ -120,7 +131,7 @@
 
                 // Because t=0 has the app icon in its original spot, we can skip the
                 // first frame and have the same movement one frame earlier.
-                mAnimator.setCurrentPlayTime(REFRESH_RATE_MS);
+                mAnimator.setCurrentPlayTime(SINGLE_FRAME_MS);
             }
         }
     }
diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index 353ed84..ab350e4 100644
--- a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -162,7 +162,8 @@
     @Override
     public ActivityOptions getActivityLaunchOptions(Launcher launcher, View v) {
         if (hasControlRemoteAppTransitionPermission()) {
-            RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mHandler) {
+            RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mHandler,
+                    true /* startAtFrontOfQueue */) {
 
                 @Override
                 public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats,
@@ -572,7 +573,7 @@
      *         ie. pressing home, swiping up from nav bar.
      */
     private RemoteAnimationRunnerCompat getWallpaperOpenRunner() {
-        return new LauncherAnimationRunner(mHandler) {
+        return new LauncherAnimationRunner(mHandler, false /* startAtFrontOfQueue */) {
             @Override
             public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats,
                     AnimationResult result) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
index d2f5487..7da50c8 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
@@ -16,7 +16,7 @@
 package com.android.launcher3.uioverrides;
 
 import static com.android.launcher3.LauncherAnimUtils.ALL_APPS_TRANSITION_MS;
-import static com.android.launcher3.allapps.DiscoveryBounce.APPS_VIEW_SHOWN;
+import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_SEEN;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
 
 import android.view.View;
@@ -47,8 +47,8 @@
 
     @Override
     public void onStateEnabled(Launcher launcher) {
-        if (!launcher.getSharedPrefs().getBoolean(APPS_VIEW_SHOWN, false)) {
-            launcher.getSharedPrefs().edit().putBoolean(APPS_VIEW_SHOWN, true).apply();
+        if (!launcher.getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false)) {
+            launcher.getSharedPrefs().edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply();
         }
 
         AbstractFloatingView.closeAllOpenViews(launcher);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
index 026f059..61422e0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.uioverrides;
 
 import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
+import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
 import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
 
@@ -25,6 +26,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.quickstep.views.RecentsView;
 
@@ -57,6 +59,10 @@
 
     @Override
     public void onStateEnabled(Launcher launcher) {
+        if (!launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)) {
+            launcher.getSharedPrefs().edit().putBoolean(HOME_BOUNCE_SEEN, true).apply();
+        }
+
         RecentsView rv = launcher.getOverviewPanel();
         rv.setOverviewStateEnabled(true);
         AbstractFloatingView.closeAllOpenViews(launcher);
@@ -71,6 +77,7 @@
     @Override
     public void onStateTransitionEnd(Launcher launcher) {
         launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE);
+        DiscoveryBounce.showForOverviewIfNeeded(launcher);
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
index 5765256..63a7984 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.uioverrides;
 
+import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 
 import android.animation.Animator;
@@ -46,7 +47,6 @@
     private static final String TAG = "OverviewSwipeController";
 
     private static final float ALLOWED_FLING_DIRECTION_CHANGE_PROGRESS = 0.1f;
-    private static final int SINGLE_FRAME_MS = 16;
 
     // Progress after which the transition is assumed to be a success in case user does not fling
     private static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index d3b0576..88cd376 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -38,11 +38,13 @@
 import com.android.launcher3.LauncherInitListener;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.allapps.AllAppsTransitionController;
+import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.util.RemoteAnimationTargetSet;
 import com.android.quickstep.views.LauncherLayoutListener;
+import com.android.quickstep.views.LauncherRecentsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -65,6 +67,8 @@
      */
     boolean onQuickInteractionStart(T activity, boolean activityVisible);
 
+    float getTranslationYForQuickScrub(T activity);
+
     void executeOnWindowAvailable(T activity, Runnable action);
 
     void onTransitionCancelled(T activity, boolean activityVisible);
@@ -98,6 +102,13 @@
      */
     boolean deferStartingActivity(int downHitTarget);
 
+    boolean supportsLongSwipe(T activity);
+
+    /**
+     * Must return a non-null controller is supportsLongSwipe was true.
+     */
+    LongSwipeHelper getLongSwipeController(T activity, RemoteAnimationTargetSet targetSet);
+
     class LauncherActivityControllerHelper implements ActivityControlHelper<Launcher> {
 
         @Override
@@ -118,6 +129,13 @@
         }
 
         @Override
+        public float getTranslationYForQuickScrub(Launcher activity) {
+            LauncherRecentsView recentsView = activity.getOverviewPanel();
+            float transYFactor = FAST_OVERVIEW.getOverviewScaleAndTranslationYFactor(activity)[1];
+            return recentsView.computeTranslationYForFactor(transYFactor);
+        }
+
+        @Override
         public void executeOnWindowAvailable(Launcher activity, Runnable action) {
             if (activity.getWorkspace().runOnOverlayHidden(action)) {
                 // Notify the activity that qiuckscrub has started
@@ -147,16 +165,19 @@
         public void onSwipeUpComplete(Launcher activity) {
             // Re apply state in case we did something funky during the transition.
             activity.getStateManager().reapplyState();
+            DiscoveryBounce.showForOverviewIfNeeded(activity);
         }
 
         @Override
         public AnimationFactory prepareRecentsUI(Launcher activity, boolean activityVisible,
                 Consumer<AnimatorPlaybackController> callback) {
-            LauncherState startState = activity.getStateManager().getState();
+            final LauncherState startState = activity.getStateManager().getState();
+
+            LauncherState resetState = startState;
             if (startState.disableRestore) {
-                startState = activity.getStateManager().getRestState();
+                resetState = activity.getStateManager().getRestState();
             }
-            activity.getStateManager().setRestState(startState);
+            activity.getStateManager().setRestState(resetState);
 
             if (!activityVisible) {
                 // Since the launcher is not visible, we can safely reset the scroll position.
@@ -168,11 +189,21 @@
                 activity.getAppsView().getContentView().setVisibility(View.GONE);
             }
 
-            return (transitionLength) ->
-                    createActivityController(activity, activityVisible, transitionLength, callback);
+            return new AnimationFactory() {
+                @Override
+                public void createActivityController(long transitionLength) {
+                    createActivityControllerInternal(activity, activityVisible, transitionLength,
+                            callback);
+                }
+
+                @Override
+                public void onTransitionCancelled() {
+                    activity.getStateManager().goToState(startState, false /* animate */);
+                }
+            };
         }
 
-        private void createActivityController(Launcher activity, boolean wasVisible,
+        private void createActivityControllerInternal(Launcher activity, boolean wasVisible,
                 long transitionLength, Consumer<AnimatorPlaybackController> callback) {
             if (wasVisible) {
                 DeviceProfile dp = activity.getDeviceProfile();
@@ -260,6 +291,20 @@
         public boolean shouldMinimizeSplitScreen() {
             return true;
         }
+
+        @Override
+        public boolean supportsLongSwipe(Launcher activity) {
+            return !activity.getDeviceProfile().isVerticalBarLayout();
+        }
+
+        @Override
+        public LongSwipeHelper getLongSwipeController(Launcher activity,
+                RemoteAnimationTargetSet targetSet) {
+            if (activity.getDeviceProfile().isVerticalBarLayout()) {
+                return null;
+            }
+            return new LongSwipeHelper(activity, targetSet);
+        }
     }
 
     class FallbackActivityControllerHelper implements ActivityControlHelper<RecentsActivity> {
@@ -276,6 +321,11 @@
         }
 
         @Override
+        public float getTranslationYForQuickScrub(RecentsActivity activity) {
+            return 0;
+        }
+
+        @Override
         public void executeOnWindowAvailable(RecentsActivity activity, Runnable action) {
             action.run();
         }
@@ -402,6 +452,17 @@
             // TODO: Remove this once b/77875376 is fixed
             return false;
         }
+
+        @Override
+        public boolean supportsLongSwipe(RecentsActivity activity) {
+            return false;
+        }
+
+        @Override
+        public LongSwipeHelper getLongSwipeController(RecentsActivity activity,
+                RemoteAnimationTargetSet targetSet) {
+            return null;
+        }
     }
 
     interface LayoutListener {
@@ -428,5 +489,7 @@
         default void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) { }
 
         void createActivityController(long transitionLength);
+
+        default void onTransitionCancelled() { }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/LongSwipeHelper.java b/quickstep/src/com/android/quickstep/LongSwipeHelper.java
new file mode 100644
index 0000000..4ce18b3
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/LongSwipeHelper.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.quickstep.WindowTransformSwipeHandler.MAX_SWIPE_DURATION;
+import static com.android.systemui.shared.recents.utilities.Utilities.getNextFrameNumber;
+import static com.android.systemui.shared.recents.utilities.Utilities.getSurface;
+
+import android.animation.ValueAnimator;
+import android.view.Surface;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.AllAppsTransitionController;
+import com.android.launcher3.allapps.DiscoveryBounce;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.quickstep.util.RemoteAnimationTargetSet;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.TransactionCompat;
+
+/**
+ * Utility class to handle long swipe from an app.
+ * This assumes the presence of Launcher activity as long swipe is not supported on the
+ * fallback activity.
+ */
+public class LongSwipeHelper {
+
+    private static final float MIN_PROGRESS_TO_ALL_APPS = 0.35f;
+    private static final float SWIPE_DURATION_MULTIPLIER =
+            Math.min(1 / MIN_PROGRESS_TO_ALL_APPS, 1 / (1 - MIN_PROGRESS_TO_ALL_APPS));
+
+    private final Launcher mLauncher;
+    private final RemoteAnimationTargetSet mTargetSet;
+
+    private float mMaxSwipeDistance = 1;
+    private AnimatorPlaybackController mAnimator;
+
+    LongSwipeHelper(Launcher launcher, RemoteAnimationTargetSet targetSet) {
+        mLauncher = launcher;
+        mTargetSet = targetSet;
+        init();
+    }
+
+    private void init() {
+        setTargetAlpha(0, true);
+
+        // Init animations
+        AllAppsTransitionController controller = mLauncher.getAllAppsController();
+        // TODO: Scale it down so that we can reach all-apps in screen space
+        mMaxSwipeDistance = Math.max(1, controller.getProgress() * controller.getShiftRange());
+        mAnimator = mLauncher.getStateManager()
+                .createAnimationToNewWorkspace(ALL_APPS, Math.round(2 * mMaxSwipeDistance));
+        mAnimator.dispatchOnStart();
+    }
+
+    public void onMove(float displacement) {
+        mAnimator.setPlayFraction(displacement / mMaxSwipeDistance);
+    }
+
+    public void destroy() {
+        // TODO: We can probably also hide the task view
+        setTargetAlpha(1, false);
+
+        mLauncher.getStateManager().goToState(OVERVIEW, false);
+    }
+
+    public void end(float velocity, boolean isFling, Runnable callback) {
+        long duration = MAX_SWIPE_DURATION;
+
+        final float currentFraction = mAnimator.getProgressFraction();
+        final boolean toAllApps;
+        float endProgress;
+
+        if (!isFling) {
+            toAllApps = currentFraction > MIN_PROGRESS_TO_ALL_APPS;
+            endProgress = toAllApps ? 1 : 0;
+
+            long expectedDuration = Math.abs(Math.round((endProgress - currentFraction)
+                    * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
+            duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
+        } else {
+            toAllApps = velocity < 0;
+            endProgress = toAllApps ? 1 : 0;
+
+            float minFlingVelocity = mLauncher.getResources()
+                    .getDimension(R.dimen.quickstep_fling_min_velocity);
+            if (Math.abs(velocity) > minFlingVelocity && mMaxSwipeDistance > 0) {
+                float distanceToTravel = (endProgress - currentFraction) * mMaxSwipeDistance;
+
+                // we want the page's snap velocity to approximately match the velocity at
+                // which the user flings, so we scale the duration by a value near to the
+                // derivative of the scroll interpolator at zero, ie. 2.
+                long baseDuration = Math.round(1000 * Math.abs(distanceToTravel / velocity));
+                duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
+            }
+        }
+
+        mAnimator.setEndAction(() -> onSwipeAnimationComplete(toAllApps, isFling, callback));
+        ValueAnimator animator = mAnimator.getAnimationPlayer();
+        animator.setDuration(duration).setInterpolator(DEACCEL);
+        animator.setFloatValues(currentFraction, endProgress);
+        animator.start();
+    }
+
+    private void setTargetAlpha(float alpha, boolean defer) {
+        final Surface surface = getSurface(mLauncher.getDragLayer());
+        final long frameNumber = defer && surface != null ? getNextFrameNumber(surface) : -1;
+        if (defer) {
+            if (frameNumber == -1) {
+                defer = false;
+            } else {
+                mLauncher.getDragLayer().invalidate();
+            }
+        }
+
+        TransactionCompat transaction = new TransactionCompat();
+        for (RemoteAnimationTargetCompat app : mTargetSet.apps) {
+            if (!(app.isNotInRecents
+                    || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME)) {
+                transaction.setAlpha(app.leash, alpha);
+                if (defer) {
+                    transaction.deferTransactionUntil(app.leash, surface, frameNumber);
+                }
+            }
+        }
+        transaction.apply();
+    }
+
+    private void onSwipeAnimationComplete(boolean toAllApps, boolean isFling, Runnable callback) {
+        mLauncher.getStateManager().goToState(toAllApps ? ALL_APPS : OVERVIEW, false);
+        if (!toAllApps) {
+            DiscoveryBounce.showForOverviewIfNeeded(mLauncher);
+        }
+
+        mLauncher.getUserEventDispatcher().logStateChangeAction(
+                isFling ? Touch.FLING : Touch.SWIPE, Direction.UP,
+                ContainerType.NAVBAR, ContainerType.APP,
+                toAllApps ? ContainerType.ALLAPPS : ContainerType.TASKSWITCHER,
+                0);
+
+        callback.run();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/MultiStateCallback.java b/quickstep/src/com/android/quickstep/MultiStateCallback.java
index 7a74176..bda3d06 100644
--- a/quickstep/src/com/android/quickstep/MultiStateCallback.java
+++ b/quickstep/src/com/android/quickstep/MultiStateCallback.java
@@ -59,4 +59,8 @@
     public int getState() {
         return mState;
     }
+
+    public boolean hasStates(int stateMask) {
+        return (mState & stateMask) == stateMask;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 0185ab9..31bead1 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -172,7 +172,8 @@
         }
 
         final TaskView taskView = (TaskView) v;
-        RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mUiHandler) {
+        RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mUiHandler,
+                true /* startAtFrontOfQueue */) {
 
             @Override
             public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats,
@@ -226,6 +227,14 @@
     }
 
     @Override
+    protected void onStop() {
+        super.onStop();
+
+        // Workaround for b/78520668, explicitly trim memory once UI is hidden
+        UiFactory.onTrimMemory(this, TRIM_MEMORY_UI_HIDDEN);
+    }
+
+    @Override
     public void onTrimMemory(int level) {
         super.onTrimMemory(level);
         UiFactory.onTrimMemory(this, level);
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java b/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java
index ebe2311..4ba9e02 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java
@@ -115,4 +115,15 @@
             }
         });
     }
+
+    public void hideCurrentInputMethod() {
+        BackgroundExecutor.get().submit(() -> {
+            synchronized (this) {
+                TraceHelper.partitionSection("RecentsController", "Hiding currentinput method");
+                if (controller != null) {
+                    controller.hideCurrentInputMethod();
+                }
+            }
+        });
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskSystemShortcut.java b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java
index e3f6543..7c69a8d 100644
--- a/quickstep/src/com/android/quickstep/TaskSystemShortcut.java
+++ b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java
@@ -150,15 +150,6 @@
                             }
                         };
 
-                final OnPreDrawListener preDrawListener = new OnPreDrawListener() {
-                    @Override
-                    public boolean onPreDraw() {
-                        taskView.getViewTreeObserver().removeOnPreDrawListener(this);
-                        WindowManagerWrapper.getInstance().endProlongedAnimations();
-                        return true;
-                    }
-                };
-
                 AbstractFloatingView.closeOpenViews(activity, true,
                         AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
 
@@ -182,7 +173,6 @@
                         //       afterwards
                         recentsView.addIgnoreResetTask(taskView);
                         taskView.setAlpha(0f);
-                        taskView.getViewTreeObserver().addOnPreDrawListener(preDrawListener);
                     };
 
                     final int[] position = new int[2];
@@ -212,6 +202,8 @@
 
     public static class Pin extends TaskSystemShortcut {
 
+        private static final String TAG = Pin.class.getSimpleName();
+
         private Handler mHandler;
 
         public Pin() {
@@ -241,6 +233,8 @@
                         } catch (RemoteException e) {
                             Log.w(TAG, "Failed to start screen pinning: ", e);
                         }
+                    } else {
+                        Log.w(TAG, taskView.getLaunchTaskFailedMsg());
                     }
                 };
                 taskView.launchTask(true, resultCallback, mHandler);
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index 25539aa..881b09b 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -31,7 +31,6 @@
 import android.annotation.TargetApi;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
-import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -52,9 +51,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.logging.UserEventDispatcher;
@@ -103,16 +100,28 @@
     private static final int STATE_HANDLER_INVALIDATED = 1 << 7;
     private static final int STATE_GESTURE_STARTED = 1 << 8;
     private static final int STATE_GESTURE_CANCELLED = 1 << 9;
+    private static final int STATE_GESTURE_COMPLETED = 1 << 10;
 
     // States for quick switch/scrub
-    private static final int STATE_SWITCH_TO_SCREENSHOT_COMPLETE = 1 << 10;
-    private static final int STATE_QUICK_SCRUB_START = 1 << 11;
-    private static final int STATE_QUICK_SCRUB_END = 1 << 12;
+    private static final int STATE_CURRENT_TASK_FINISHED = 1 << 11;
+    private static final int STATE_QUICK_SCRUB_START = 1 << 12;
+    private static final int STATE_QUICK_SCRUB_END = 1 << 13;
+
+    private static final int STATE_CAPTURE_SCREENSHOT = 1 << 14;
+    private static final int STATE_SCREENSHOT_CAPTURED = 1 << 15;
 
     private static final int LAUNCHER_UI_STATES =
             STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_ACTIVITY_MULTIPLIER_COMPLETE
             | STATE_LAUNCHER_STARTED;
 
+    private static final int LONG_SWIPE_ENTER_STATE =
+            STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED
+                    | STATE_APP_CONTROLLER_RECEIVED;
+
+    private static final int LONG_SWIPE_START_STATE =
+            STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED
+                    | STATE_APP_CONTROLLER_RECEIVED | STATE_SCREENSHOT_CAPTURED;
+
     // For debugging, keep in sync with above states
     private static final String[] STATES = new String[] {
             "STATE_LAUNCHER_PRESENT",
@@ -125,14 +134,16 @@
             "STATE_HANDLER_INVALIDATED",
             "STATE_GESTURE_STARTED",
             "STATE_GESTURE_CANCELLED",
-            "STATE_SWITCH_TO_SCREENSHOT_COMPLETE",
-            "STATE_QUICK_SWITCH",
+            "STATE_GESTURE_COMPLETED",
+            "STATE_CURRENT_TASK_FINISHED",
             "STATE_QUICK_SCRUB_START",
-            "STATE_QUICK_SCRUB_END"
+            "STATE_QUICK_SCRUB_END",
+            "STATE_CAPTURE_SCREENSHOT",
+            "STATE_SCREENSHOT_CAPTURED",
     };
 
-    private static final long MAX_SWIPE_DURATION = 350;
-    private static final long MIN_SWIPE_DURATION = 80;
+    public static final long MAX_SWIPE_DURATION = 350;
+    public static final long MIN_SWIPE_DURATION = 80;
 
     private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
     private static final float SWIPE_DURATION_MULTIPLIER =
@@ -151,7 +162,7 @@
     // visible.
     private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
 
-    private final MainThreadExecutor mMainExecutor = new MainThreadExecutor();
+    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
 
     private final Context mContext;
     private final int mRunningTaskId;
@@ -171,7 +182,6 @@
 
     private boolean mWasLauncherAlreadyVisible;
 
-    private float mCurrentDisplacement;
     private boolean mGestureStarted;
     private int mLogAction = Touch.SWIPE;
     private float mCurrentQuickScrubProgress;
@@ -185,6 +195,11 @@
     private final long mTouchTimeMs;
     private long mLauncherFrameDrawnTime;
 
+    private boolean mBgLongSwipeMode = false;
+    private boolean mUiLongSwipeMode = false;
+    private float mLongSwipeDisplacement = 0;
+    private LongSwipeHelper mLongSwipeController;
+
     WindowTransformSwipeHandler(RunningTaskInfo runningTaskInfo, Context context, long touchTimeMs,
             ActivityControlHelper<T> controller) {
         mContext = context;
@@ -194,10 +209,10 @@
         mActivityInitListener = mActivityControlHelper
                 .createActivityInitListener(this::onActivityInit);
 
+        initStateCallbacks();
         // Register the input consumer on the UI thread, to ensure that it runs after any pending
         // unregister calls
-        mMainExecutor.execute(mInputConsumer::registerInputConsumer);
-        initStateCallbacks();
+        executeOnUiThread(mInputConsumer::registerInputConsumer);
     }
 
     private void initStateCallbacks() {
@@ -226,12 +241,18 @@
                 this::resumeLastTask);
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
                         | STATE_ACTIVITY_MULTIPLIER_COMPLETE
-                        | STATE_SCALED_CONTROLLER_RECENTS,
+                        | STATE_CAPTURE_SCREENSHOT,
                 this::switchToScreenshot);
+
+        mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
+                        | STATE_SCALED_CONTROLLER_RECENTS,
+                this::finishCurrentTransitionToHome);
+
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
                         | STATE_ACTIVITY_MULTIPLIER_COMPLETE
                         | STATE_SCALED_CONTROLLER_RECENTS
-                        | STATE_SWITCH_TO_SCREENSHOT_COMPLETE,
+                        | STATE_CURRENT_TASK_FINISHED
+                        | STATE_GESTURE_COMPLETED,
                 this::setupLauncherUiAfterSwipeUpAnimation);
 
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_APP,
@@ -240,21 +261,34 @@
         mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
         mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
                 this::invalidateHandlerWithLauncher);
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED
+                | STATE_SCALED_CONTROLLER_APP,
+                this::notifyTransitionCancelled);
 
         mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START,
                 this::onQuickScrubStart);
         mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START
                 | STATE_SCALED_CONTROLLER_RECENTS, this::onFinishedTransitionToQuickScrub);
-        mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_SWITCH_TO_SCREENSHOT_COMPLETE
+        mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_CURRENT_TASK_FINISHED
                 | STATE_QUICK_SCRUB_END, this::switchToFinalAppAfterQuickScrub);
+
+        mStateCallback.addCallback(LONG_SWIPE_ENTER_STATE, this::checkLongSwipeCanEnter);
+        mStateCallback.addCallback(LONG_SWIPE_START_STATE, this::checkLongSwipeCanStart);
+    }
+
+    private void executeOnUiThread(Runnable action) {
+        if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
+            action.run();
+        } else {
+            postAsyncCallback(mMainThreadHandler, action);
+        }
     }
 
     private void setStateOnUiThread(int stateFlag) {
-        Handler handler = mMainExecutor.getHandler();
-        if (Looper.myLooper() == handler.getLooper()) {
+        if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
             mStateCallback.setState(stateFlag);
         } else {
-            postAsyncCallback(handler, () -> mStateCallback.setState(stateFlag));
+            postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag));
         }
     }
 
@@ -321,7 +355,7 @@
         if (mActivity != activity) {
             return;
         }
-        if ((mStateCallback.getState() & STATE_HANDLER_INVALIDATED) != 0) {
+        if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
             return;
         }
 
@@ -416,11 +450,26 @@
 
     @WorkerThread
     public void updateDisplacement(float displacement) {
-        mCurrentDisplacement = displacement;
+        // We are moving in the negative x/y direction
+        displacement = -displacement;
+        if (displacement > mTransitionDragLength) {
+            mCurrentShift.updateValue(1);
 
-        float translation = Utilities.boundToRange(-mCurrentDisplacement, 0, mTransitionDragLength);
-        float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
-        mCurrentShift.updateValue(shift);
+            if (!mBgLongSwipeMode) {
+                mBgLongSwipeMode = true;
+                executeOnUiThread(this::onLongSwipeEnabledUi);
+            }
+            mLongSwipeDisplacement = displacement - mTransitionDragLength;
+            executeOnUiThread(this::onLongSwipeDisplacementUpdated);
+        } else {
+            if (mBgLongSwipeMode) {
+                mBgLongSwipeMode = false;
+                executeOnUiThread(this::onLongSwipeDisabledUi);
+            }
+            float translation = Math.max(displacement, 0);
+            float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
+            mCurrentShift.updateValue(shift);
+        }
     }
 
     /**
@@ -448,44 +497,25 @@
                 float interpolated = interpolator.getInterpolation(shift);
                 mClipAnimationHelper.applyTransform(
                         mRecentsAnimationWrapper.targetSet, interpolated);
+
+                // TODO: This logic is spartanic!
+                boolean passedThreshold = shift > 0.12f;
+                mRecentsAnimationWrapper.setAnimationTargetsBehindSystemBars(!passedThreshold);
+                if (mActivityControlHelper.shouldMinimizeSplitScreen()) {
+                    mRecentsAnimationWrapper
+                            .setSplitScreenMinimizedForTransaction(passedThreshold);
+                }
             }
         }
 
-        if (mLauncherTransitionController != null) {
-            Runnable runOnUi = () -> {
-                if (mLauncherTransitionController == null) {
-                    return;
-                }
-                mLauncherTransitionController.setPlayFraction(shift);
+        executeOnUiThread(this::updateFinalShiftUi);
+    }
 
-                // Make sure the window follows the first task if it moves, e.g. during quick scrub.
-                View firstTask = mRecentsView.getPageAt(0);
-                // The first task may be null if we are swiping up from a task that does not
-                // appear in the list (ie. the assistant)
-                if (firstTask != null) {
-                    int scrollForFirstTask = mRecentsView.getScrollForPage(0);
-                    int offsetFromFirstTask = (scrollForFirstTask - mRecentsView.getScrollX());
-                    mClipAnimationHelper.offsetTarget(firstTask.getScaleX(),
-                            offsetFromFirstTask + firstTask.getTranslationX(),
-                            mRecentsView.getTranslationY());
-                }
-                if (mRecentsAnimationWrapper.controller != null) {
-                    // TODO: This logic is spartanic!
-                    boolean passedThreshold = shift > 0.12f;
-                    mRecentsAnimationWrapper.setAnimationTargetsBehindSystemBars(!passedThreshold);
-                    if (mActivityControlHelper.shouldMinimizeSplitScreen()) {
-                        mRecentsAnimationWrapper
-                                .setSplitScreenMinimizedForTransaction(passedThreshold);
-                    }
-                }
-            };
-            if (Looper.getMainLooper() == Looper.myLooper()) {
-                runOnUi.run();
-            } else {
-                // The fling operation completed even before the launcher was drawn
-                mMainExecutor.execute(runOnUi);
-            }
+    private void updateFinalShiftUi() {
+        if (mLauncherTransitionController == null) {
+            return;
         }
+        mLauncherTransitionController.setPlayFraction(mCurrentShift.value);
     }
 
     public void onRecentsAnimationStart(RecentsAnimationControllerCompat controller,
@@ -532,6 +562,7 @@
         notifyGestureStartedAsync();
         setStateOnUiThread(STATE_GESTURE_STARTED);
         mGestureStarted = true;
+        mRecentsAnimationWrapper.hideCurrentInputMethod();
         mRecentsAnimationWrapper.enableInputConsumer();
         ActivityManagerWrapper.getInstance().closeSystemWindows(
                 CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
@@ -555,10 +586,21 @@
 
     @WorkerThread
     public void onGestureEnded(float endVelocity) {
-        Resources res = mContext.getResources();
-        float flingThreshold = res.getDimension(R.dimen.quickstep_fling_threshold_velocity);
+        float flingThreshold = mContext.getResources()
+                .getDimension(R.dimen.quickstep_fling_threshold_velocity);
         boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold;
+        setStateOnUiThread(STATE_GESTURE_COMPLETED);
 
+        mLogAction = isFling ? Touch.FLING : Touch.SWIPE;
+
+        if (mBgLongSwipeMode) {
+            executeOnUiThread(() -> onLongSwipeGestureFinishUi(endVelocity, isFling));
+        } else {
+            handleNormalGestureEnd(endVelocity, isFling);
+        }
+    }
+
+    private void handleNormalGestureEnd(float endVelocity, boolean isFling) {
         long duration = MAX_SWIPE_DURATION;
         final float endShift;
         if (!isFling) {
@@ -566,10 +608,10 @@
             long expectedDuration = Math.abs(Math.round((endShift - mCurrentShift.value)
                     * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
             duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
-            mLogAction = Touch.SWIPE;
         } else {
             endShift = endVelocity < 0 ? 1 : 0;
-            float minFlingVelocity = res.getDimension(R.dimen.quickstep_fling_min_velocity);
+            float minFlingVelocity = mContext.getResources()
+                    .getDimension(R.dimen.quickstep_fling_min_velocity);
             if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) {
                 float distanceToTravel = (endShift - mCurrentShift.value) * mTransitionDragLength;
 
@@ -579,7 +621,6 @@
                 long baseDuration = Math.round(1000 * Math.abs(distanceToTravel / endVelocity));
                 duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
             }
-            mLogAction = Touch.FLING;
         }
 
         animateToProgress(endShift, duration, DEACCEL);
@@ -609,8 +650,9 @@
         anim.addListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationSuccess(Animator animator) {
-                setStateOnUiThread(mIsGoingToHome ?
-                        STATE_SCALED_CONTROLLER_RECENTS : STATE_SCALED_CONTROLLER_APP);
+                setStateOnUiThread(mIsGoingToHome
+                        ? (STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT)
+                        : STATE_SCALED_CONTROLLER_APP);
             }
         });
         anim.start();
@@ -649,6 +691,10 @@
         mRecentsView.setFirstTaskIconScaledDown(false /* isScaledDown */, false /* animate */);
     }
 
+    private void notifyTransitionCancelled() {
+        mAnimationFactory.onTransitionCancelled();
+    }
+
     private void resetStateForAnimationCancel() {
         boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted;
         mActivityControlHelper.onTransitionCancelled(mActivity, wasVisible);
@@ -662,13 +708,6 @@
 
     private void switchToScreenshot() {
         boolean finishTransitionPosted = false;
-        final Runnable finishTransitionRunnable = () -> {
-            synchronized (mRecentsAnimationWrapper) {
-                mRecentsAnimationWrapper.finish(true /* toHome */,
-                        () -> setStateOnUiThread(STATE_SWITCH_TO_SCREENSHOT_COMPLETE));
-            }
-        };
-
         synchronized (mRecentsAnimationWrapper) {
             if (mRecentsAnimationWrapper.controller != null) {
                 // Update the screenshot of the task
@@ -683,7 +722,7 @@
 
                         @Override
                         public void onPostDraw(Canvas canvas) {
-                            finishTransitionRunnable.run();
+                            setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
                             detach();
                         }
                     }.attach();
@@ -691,11 +730,16 @@
             }
         }
         if (!finishTransitionPosted) {
-            // If we haven't posted the transition end runnable, run it now
-            finishTransitionRunnable.run();
+            // If we haven't posted a draw callback, set the state immediately.
+            setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
         }
-        RecentsModel.getInstance(mContext).onOverviewShown(false, TAG);
-        doLogGesture(true /* toLauncher */);
+    }
+
+    private void finishCurrentTransitionToHome() {
+        synchronized (mRecentsAnimationWrapper) {
+            mRecentsAnimationWrapper.finish(true /* toHome */,
+                    () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
+        }
     }
 
     private void setupLauncherUiAfterSwipeUpAnimation() {
@@ -707,18 +751,40 @@
 
         // Animate the first icon.
         mRecentsView.setFirstTaskIconScaledDown(false /* isScaledDown */, true /* animate */);
-
         mRecentsView.setSwipeDownShouldLaunchApp(true);
 
+        RecentsModel.getInstance(mContext).onOverviewShown(false, TAG);
+
+        doLogGesture(true /* toLauncher */);
         reset();
     }
 
     private void onQuickScrubStart() {
-        mActivityControlHelper.onQuickInteractionStart(mActivity, mWasLauncherAlreadyVisible);
+        if (mLauncherTransitionController != null) {
+            mLauncherTransitionController.getAnimationPlayer().end();
+            mLauncherTransitionController = null;
+        }
+
+        mActivityControlHelper.onQuickInteractionStart(mActivity, false);
         mQuickScrubController.onQuickScrubStart(false);
 
         // Inform the last progress in case we skipped before.
         mQuickScrubController.onQuickScrubProgress(mCurrentQuickScrubProgress);
+
+        // Make sure the window follows the first task if it moves, e.g. during quick scrub.
+        TaskView firstTask = mRecentsView.getPageAt(0);
+        // The first task may be null if we are swiping up from a task that does not
+        // appear in the list (i.e. the assistant)
+        if (firstTask != null) {
+            int scrollForFirstTask = mRecentsView.getScrollForPage(0);
+            int scrollForSecondTask = mRecentsView.getChildCount() > 1
+                    ? mRecentsView.getScrollForPage(1) : scrollForFirstTask;
+            int offsetFromFirstTask = scrollForFirstTask - scrollForSecondTask;
+            float interpolation = offsetFromFirstTask / (mRecentsView.getWidth() / 2);
+            mClipAnimationHelper.offsetTarget(
+                    firstTask.getCurveScaleForInterpolation(interpolation), offsetFromFirstTask,
+                    mActivityControlHelper.getTranslationYForQuickScrub(mActivity));
+        }
     }
 
     private void onFinishedTransitionToQuickScrub() {
@@ -769,4 +835,64 @@
     public void setGestureEndCallback(Runnable gestureEndCallback) {
         mGestureEndCallback = gestureEndCallback;
     }
+
+    // Handling long swipe
+    private void onLongSwipeEnabledUi() {
+        mUiLongSwipeMode = true;
+        checkLongSwipeCanEnter();
+        checkLongSwipeCanStart();
+    }
+
+    private void onLongSwipeDisabledUi() {
+        mUiLongSwipeMode = false;
+
+        if (mLongSwipeController != null) {
+            mLongSwipeController.destroy();
+
+            // Rebuild animations
+            buildAnimationController();
+        }
+    }
+
+    private void onLongSwipeDisplacementUpdated() {
+        if (!mUiLongSwipeMode || mLongSwipeController == null) {
+            return;
+        }
+
+        mLongSwipeController.onMove(mLongSwipeDisplacement);
+    }
+
+    private void checkLongSwipeCanEnter() {
+        if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_ENTER_STATE)
+                || !mActivityControlHelper.supportsLongSwipe(mActivity)) {
+            return;
+        }
+
+        // We are entering long swipe mode, make sure the screen shot is captured.
+        mStateCallback.setState(STATE_CAPTURE_SCREENSHOT);
+
+    }
+
+    private void checkLongSwipeCanStart() {
+        if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_START_STATE)
+                || !mActivityControlHelper.supportsLongSwipe(mActivity)) {
+            return;
+        }
+
+        mLongSwipeController = mActivityControlHelper.getLongSwipeController(
+                mActivity, mRecentsAnimationWrapper.targetSet);
+        onLongSwipeDisplacementUpdated();
+    }
+
+    private void onLongSwipeGestureFinishUi(float velocity, boolean isFling) {
+        if (!mUiLongSwipeMode || mLongSwipeController == null) {
+            handleNormalGestureEnd(velocity, isFling);
+            return;
+        }
+
+        finishCurrentTransitionToHome();
+        mLongSwipeController.end(velocity, isFling,
+                () -> setStateOnUiThread(STATE_HANDLER_INVALIDATED));
+
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index a4d3a50..fb4aa02 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -35,7 +35,6 @@
     public FallbackRecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         setOverviewStateEnabled(true);
-        updateEmptyMessage();
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
index fb7a850..057e0c4 100644
--- a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
+++ b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java
@@ -15,9 +15,13 @@
  */
 package com.android.quickstep.util;
 
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.SCROLL;
+
 import android.graphics.Canvas;
 import android.graphics.Matrix;
 import android.graphics.Matrix.ScaleToFit;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
 
@@ -46,8 +50,8 @@
     private final RectF mSourceRect = new RectF();
     // The bounds of the task view in launcher window coordinates
     private final RectF mTargetRect = new RectF();
-    // Doesn't change after initialized, used as an anchor when changing mTargetRect
-    private final RectF mInitialTargetRect = new RectF();
+    // Set when the final window destination is changed, such as offsetting for quick scrub
+    private final PointF mTargetOffset = new PointF();
     // The insets to be used for clipping the app window, which can be larger than mSourceInsets
     // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In
     // app window coordinates.
@@ -60,6 +64,9 @@
     private final Rect mClipRect = new Rect();
     private final RectFEvaluator mRectFEvaluator = new RectFEvaluator();
     private final Matrix mTmpMatrix = new Matrix();
+    private final RectF mTmpRectF = new RectF();
+
+    private float mTargetScale = 1f;
 
     public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) {
         mHomeStackBounds.set(homeStackBounds);
@@ -78,8 +85,6 @@
         mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left,
                 mHomeStackBounds.top - mSourceStackBounds.top);
 
-        mInitialTargetRect.set(mTargetRect);
-
         // Calculate the clip based on the target rect (since the content insets and the
         // launcher insets may differ, so the aspect ratio of the target rect can differ
         // from the source rect. The difference between the target rect (scaled to the
@@ -98,10 +103,14 @@
 
     public void applyTransform(RemoteAnimationTargetSet targetSet, float progress) {
         RectF currentRect;
-        synchronized (mTargetRect) {
-            currentRect =  mRectFEvaluator.evaluate(progress, mSourceRect, mTargetRect);
+        mTmpRectF.set(mTargetRect);
+        Utilities.scaleRectFAboutCenter(mTmpRectF, mTargetScale);
+        currentRect =  mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF);
+
+        synchronized (mTargetOffset) {
             // Stay lined up with the center of the target, since it moves for quick scrub.
-            currentRect.offset(mTargetRect.centerX() - currentRect.centerX(), 0);
+            currentRect.offset(mTargetOffset.x * SCROLL.getInterpolation(progress),
+                    mTargetOffset.y  * LINEAR.getInterpolation(progress));
         }
 
         mClipRect.left = (int) (mSourceWindowClipInsets.left * progress);
@@ -131,10 +140,9 @@
     }
 
     public void offsetTarget(float scale, float offsetX, float offsetY) {
-        synchronized (mTargetRect) {
-            mTargetRect.set(mInitialTargetRect);
-            Utilities.scaleRectFAboutCenter(mTargetRect, scale);
-            mTargetRect.offset(offsetX, offsetY);
+        synchronized (mTargetOffset) {
+            mTargetScale = scale;
+            mTargetOffset.set(offsetX, offsetY);
         }
     }
 
@@ -187,14 +195,11 @@
     }
 
     public void drawForProgress(TaskThumbnailView ttv, Canvas canvas, float progress) {
-        RectF currentRect;
-        synchronized (mTargetRect) {
-            currentRect =  mRectFEvaluator.evaluate(progress, mSourceRect, mTargetRect);
-        }
-
+        RectF currentRect =  mRectFEvaluator.evaluate(progress, mSourceRect, mTargetRect);
         canvas.translate(mSourceStackBounds.left - mHomeStackBounds.left,
                 mSourceStackBounds.top - mHomeStackBounds.top);
         mTmpMatrix.setRectToRect(mTargetRect, currentRect, ScaleToFit.FILL);
+
         canvas.concat(mTmpMatrix);
         canvas.translate(mTargetRect.left, mTargetRect.top);
 
diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
index 7ee16c1..dfe1984 100644
--- a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
+++ b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
@@ -31,7 +31,8 @@
     AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targets);
 
     default ActivityOptions toActivityOptions(Handler handler, long duration) {
-        LauncherAnimationRunner runner = new LauncherAnimationRunner(handler) {
+        LauncherAnimationRunner runner = new LauncherAnimationRunner(handler,
+                false /* startAtFrontOfQueue */) {
 
             @Override
             public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats,
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 06e5311..90749eb 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -35,6 +35,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
+import com.android.quickstep.OverviewInteractionState;
 import com.android.quickstep.util.LayoutUtils;
 
 /**
@@ -86,7 +87,11 @@
 
     public void setTranslationYFactor(float translationFactor) {
         mTranslationYFactor = translationFactor;
-        setTranslationY(mTranslationYFactor * (getPaddingBottom() - getPaddingTop()));
+        setTranslationY(computeTranslationYForFactor(mTranslationYFactor));
+    }
+
+    public float computeTranslationYForFactor(float translationYFactor) {
+        return translationYFactor * (getPaddingBottom() - getPaddingTop());
     }
 
     @Override
@@ -114,6 +119,12 @@
     public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv) {
         AnimatorSet anim = super.createAdjacentPageAnimForTaskLaunch(tv);
 
+        if (!OverviewInteractionState.getInstance(mActivity).isSwipeUpGestureEnabled()) {
+            // Hotseat doesn't move when opening recents with the button,
+            // so don't animate it here either.
+            return anim;
+        }
+
         float allAppsProgressOffscreen = ALL_APPS_PROGRESS_OFF_SCREEN;
         LauncherState state = mActivity.getStateManager().getState();
         if ((state.getVisibleElements(mActivity) & ALL_APPS_HEADER_EXTRA) != 0) {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index a7b018a..5fff51c 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -30,8 +30,6 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.graphics.Canvas;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -43,6 +41,7 @@
 import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
+import android.util.Log;
 import android.util.SparseBooleanArray;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
@@ -85,8 +84,9 @@
  * A list of recent tasks.
  */
 @TargetApi(Build.VERSION_CODES.P)
-public abstract class RecentsView<T extends BaseActivity>
-        extends PagedView implements OnSharedPreferenceChangeListener, Insettable {
+public abstract class RecentsView<T extends BaseActivity> extends PagedView implements Insettable {
+
+    private static final String TAG = RecentsView.class.getSimpleName();
 
     public static final boolean DEBUG_SHOW_CLEAR_ALL_BUTTON = false;
 
@@ -104,7 +104,7 @@
             return recentsView.mAdjacentScale;
         }
     };
-    private static final String PREF_FLIP_RECENTS = "pref_flip_recents";
+    private static final boolean FLIP_RECENTS = true;
     private static final int DISMISS_TASK_DURATION = 300;
 
     protected final T mActivity;
@@ -201,7 +201,11 @@
         mQuickScrubController = new QuickScrubController(mActivity, this);
         mModel = RecentsModel.getInstance(context);
 
-        onSharedPreferenceChanged(Utilities.getPrefs(context), PREF_FLIP_RECENTS);
+        mIsRtl = Utilities.isRtl(getResources());
+        if (FLIP_RECENTS) {
+            mIsRtl = !mIsRtl;
+        }
+        setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
 
         mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents);
         mEmptyIcon.setCallback(this);
@@ -213,17 +217,7 @@
         mEmptyMessagePadding = getResources()
                 .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
         setWillNotDraw(false);
-    }
-
-    @Override
-    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
-        if (s.equals(PREF_FLIP_RECENTS)) {
-            mIsRtl = Utilities.isRtl(getResources());
-            if (sharedPreferences.getBoolean(PREF_FLIP_RECENTS, false)) {
-                mIsRtl = !mIsRtl;
-            }
-            setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
-        }
+        updateEmptyMessage();
     }
 
     public boolean isRtl() {
@@ -248,7 +242,6 @@
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         updateTaskStackListenerState();
-        Utilities.getPrefs(getContext()).registerOnSharedPreferenceChangeListener(this);
         mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
     }
 
@@ -256,7 +249,6 @@
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         updateTaskStackListenerState();
-        Utilities.getPrefs(getContext()).unregisterOnSharedPreferenceChangeListener(this);
         mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
     }
 
@@ -310,6 +302,56 @@
         }
     }
 
+    private float calculateClearAllButtonAlpha() {
+        if (mClearAllButton.getVisibility() != View.VISIBLE || getChildCount() == 0) return 0;
+
+        // Current visible coordinate of the right border of the rightmost task.
+        final int carouselCurrentRight = getChildAt(getChildCount() - 1).getRight() - getScrollX();
+
+        // As the right border (let's call it E aka carouselCurrentRight) of the carousel moves
+        // over Clear all button, the button changes trasparency.
+        // leftOfAlphaChange < rightOfAlphaChange; these are the points of the 100% and 0% alpha
+        // correspondingly. Alpha changes linearly between 100% and 0% as E moves through this
+        // range. It doesn't change outside of the range.
+
+        // Once E hits the left border of the Clear-All button, the whole button is uncovered,
+        // and it should have alpha 100%.
+        final float leftOfAlphaChange = mClearAllButton.getX();
+
+        // The rightmost possible right coordinate of the carousel.
+        final int carouselMotionLimit = getScrollForPage(getChildCount() - 1) + getWidth()
+                - getPaddingRight() - mInsets.right;
+
+        // The carousel might not be able to ever cover a part of the Clear-all button. Then
+        // always show the button as 100%. Technically, this check also prevents dividing by zero
+        // or a negative number when calculating the transparency ratio below.
+        if (carouselMotionLimit <= leftOfAlphaChange) return 1;
+
+        // If the carousel is able to cover the button completely, we make the button completely
+        // transparent when E hits the right border of the button.
+        // Or, the carousel may not be able to move that far to the right so it completely covers
+        // the button. Then we set the rightmost possible position of the carousel as the point
+        // where the button reaches 0 alpha.
+        final float rightOfAlphaChange = Math.min(
+                mClearAllButton.getX() + mClearAllButton.getWidth(), carouselMotionLimit);
+
+        return Utilities.boundToRange(
+                (rightOfAlphaChange - carouselCurrentRight) /
+                        (rightOfAlphaChange - leftOfAlphaChange), 0, 1);
+    }
+
+    private void updateClearAllButtonAlpha() {
+        if (mClearAllButton != null) {
+            mClearAllButton.setAlpha(calculateClearAllButtonAlpha() * mContentAlpha);
+        }
+    }
+
+    @Override
+    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+        super.onScrollChanged(l, t, oldl, oldt);
+        updateClearAllButtonAlpha();
+    }
+
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
         if (DEBUG_SHOW_CLEAR_ALL_BUTTON && ev.getAction() == MotionEvent.ACTION_DOWN
@@ -674,8 +716,23 @@
         mIgnoreResetTaskViews.remove(taskView);
     }
 
+    private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) {
+        addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim);
+        addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()),
+                duration, LINEAR, anim);
+    }
+
+    private void removeTask(Task task, PendingAnimation.OnEndListener onEndListener) {
+        if (task != null) {
+            ActivityManagerWrapper.getInstance().removeTask(task.key.id);
+            mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
+                    onEndListener.logAction, Direction.UP,
+                    TaskUtils.getComponentKeyForTask(task.key));
+        }
+    }
+
     public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView,
-            boolean removeTask, long duration) {
+            boolean shouldRemoveTask, long duration) {
         if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) {
             throw new IllegalStateException("Another pending animation is still running");
         }
@@ -707,9 +764,7 @@
             View child = getChildAt(i);
             if (child == taskView) {
                 if (animateTaskView) {
-                    addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim);
-                    addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()),
-                            duration, LINEAR, anim);
+                    addDismissedTaskAnimations(taskView, anim, duration);
                 }
             } else {
                 // If we just take newScroll - oldScroll, everything to the right of dragged task
@@ -754,14 +809,8 @@
         mPendingAnimation = pendingAnimation;
         mPendingAnimation.addEndListener((onEndListener) -> {
            if (onEndListener.isSuccess) {
-               if (removeTask) {
-                   Task task = taskView.getTask();
-                   if (task != null) {
-                       ActivityManagerWrapper.getInstance().removeTask(task.key.id);
-                       mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
-                               onEndListener.logAction, Direction.UP,
-                               TaskUtils.getComponentKeyForTask(task.key));
-                   }
+               if (shouldRemoveTask) {
+                   removeTask(taskView.getTask(), onEndListener);
                }
                int pageToSnapTo = mCurrentPage;
                if (draggedIndex < pageToSnapTo) {
@@ -780,6 +829,33 @@
         return pendingAnimation;
     }
 
+    public PendingAnimation createAllTasksDismissAnimation(long duration) {
+        if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) {
+            throw new IllegalStateException("Another pending animation is still running");
+        }
+        AnimatorSet anim = new AnimatorSet();
+        PendingAnimation pendingAnimation = new PendingAnimation(anim);
+
+        int count = getChildCount();
+        for (int i = 0; i < count; i++) {
+            addDismissedTaskAnimations(getChildAt(i), anim, duration);
+        }
+
+        mPendingAnimation = pendingAnimation;
+        mPendingAnimation.addEndListener((onEndListener) -> {
+            if (onEndListener.isSuccess) {
+                while (getChildCount() != 0) {
+                    TaskView taskView = getPageAt(getChildCount() - 1);
+                    removeTask(taskView.getTask(), onEndListener);
+                    removeView(taskView);
+                }
+                onAllTasksRemoved();
+            }
+            mPendingAnimation = null;
+        });
+        return pendingAnimation;
+    }
+
     private static void addAnim(ObjectAnimator anim, long duration,
             TimeInterpolator interpolator, AnimatorSet set) {
         anim.setDuration(duration).setInterpolator(interpolator);
@@ -803,9 +879,7 @@
         }
     }
 
-    public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) {
-        PendingAnimation pendingAnim = createTaskDismissAnimation(taskView, animateTaskView,
-                removeTask, DISMISS_TASK_DURATION);
+    private void runDismissAnimation(PendingAnimation pendingAnim) {
         AnimatorPlaybackController controller = AnimatorPlaybackController.wrap(
                 pendingAnim.anim, DISMISS_TASK_DURATION);
         controller.dispatchOnStart();
@@ -814,6 +888,15 @@
         controller.start();
     }
 
+    public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) {
+        runDismissAnimation(createTaskDismissAnimation(taskView, animateTaskView, removeTask,
+                DISMISS_TASK_DURATION));
+    }
+
+    public void dismissAllTasks() {
+        runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION));
+    }
+
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
         if (event.getAction() == KeyEvent.ACTION_DOWN) {
@@ -864,6 +947,7 @@
         int alphaInt = Math.round(alpha * 255);
         mEmptyMessagePaint.setAlpha(alphaInt);
         mEmptyIcon.setAlpha(alphaInt);
+        updateClearAllButtonAlpha();
     }
 
     public void setAdjacentScale(float adjacentScale) {
@@ -948,6 +1032,7 @@
             mEmptyTextLayout = null;
             mLastMeasureSize.set(getWidth(), getHeight());
         }
+        updateClearAllButtonVisibility();
 
         if (!mShowEmptyMessage) return;
 
@@ -1074,10 +1159,13 @@
         anim.play(drawableAnim);
         anim.setDuration(duration);
 
-        Consumer<Boolean> onTaskLaunchFinish = (r) -> {
-            onTaskLaunched(r);
+        Consumer<Boolean> onTaskLaunchFinish = (result) -> {
+            onTaskLaunched(result);
             tv.setVisibility(VISIBLE);
             getOverlay().remove(drawable);
+            if (!result) {
+                Log.w(TAG, tv.getLaunchTaskFailedMsg());
+            }
         };
 
         mPendingAnimation = new PendingAnimation(anim);
@@ -1118,16 +1206,6 @@
         return "";
     }
 
-    public void dismissAllTasks() {
-        for (int i = 0; i < getChildCount(); ++i) {
-            Task task = getPageAt(i).getTask();
-            if (task != null) {
-                ActivityManagerWrapper.getInstance().removeTask(task.key.id);
-            }
-        }
-        onAllTasksRemoved();
-    }
-
     @Override
     protected int computeMaxScrollX() {
         if (!DEBUG_SHOW_CLEAR_ALL_BUTTON || getChildCount() == 0) {
@@ -1140,8 +1218,10 @@
     }
 
     private void updateClearAllButtonVisibility() {
+        if (mClearAllButton == null) return;
         mClearAllButton.setVisibility(
                 !DEBUG_SHOW_CLEAR_ALL_BUTTON || mShowEmptyMessage ? GONE : VISIBLE);
+        updateClearAllButtonAlpha();
     }
 
     public void setClearAllButton(View clearAllButton) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index 592166d..c724930 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -16,6 +16,8 @@
 
 package com.android.quickstep.views;
 
+import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
+
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
@@ -206,7 +208,8 @@
                 // Rotate the screenshot if not in multi-window mode
                 rotate = FeatureFlags.OVERVIEW_USE_SCREENSHOT_ORIENTATION &&
                         configuration.orientation != mThumbnailData.orientation &&
-                        !mActivity.isInMultiWindowModeCompat();
+                        !mActivity.isInMultiWindowModeCompat() &&
+                        mThumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
                 // Scale the screenshot to always fit the width of the card.
                 thumbnailScale = rotate
                         ? getMeasuredWidth() / thumbnailHeight
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index b32d8dd..a9b24e5 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -30,6 +30,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewOutlineProvider;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -59,6 +60,8 @@
  */
 public class TaskView extends FrameLayout implements TaskCallbacks, PageCallbacks {
 
+    private static final String TAG = TaskView.class.getSimpleName();
+
     /** A curve of x from 0 to 1, where 0 is the center of the screen and 1 is the edge. */
     private static final TimeInterpolator CURVE_INTERPOLATOR
             = x -> (float) -Math.cos(x * Math.PI) / 2f + .5f;
@@ -136,7 +139,11 @@
     }
 
     public void launchTask(boolean animate) {
-        launchTask(animate, null, null);
+        launchTask(animate, (result) -> {
+            if (!result) {
+                Log.w(TAG, getLaunchTaskFailedMsg());
+            }
+        }, getHandler());
     }
 
     public void launchTask(boolean animate, Consumer<Boolean> resultCallback,
@@ -219,11 +226,20 @@
             mSnapshotView.setDimAlpha(mCurveDimAlpha);
         }
 
-        mCurveScale = 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR;
+        mCurveScale = getCurveScaleForCurveInterpolation(curveInterpolation);
         setScaleX(mCurveScale);
         setScaleY(mCurveScale);
     }
 
+    public float getCurveScaleForInterpolation(float linearInterpolation) {
+        float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(linearInterpolation);
+        return getCurveScaleForCurveInterpolation(curveInterpolation);
+    }
+
+    private float getCurveScaleForCurveInterpolation(float curveInterpolation) {
+        return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR;
+    }
+
     public float getCurveScale() {
         return mCurveScale;
     }
@@ -308,4 +324,12 @@
 
         return super.performAccessibilityAction(action, arguments);
     }
+
+    public String getLaunchTaskFailedMsg() {
+        String msg = "Failed to launch task";
+        if (mTask != null) {
+            msg += " (task=" + mTask.key.baseIntent + " userId=" + mTask.key.userId + ")";
+        }
+        return msg;
+    }
 }
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 693cf31..097c341 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -97,6 +97,7 @@
         handleClose(animate);
         BaseActivity.fromContext(getContext()).getUserEventDispatcher()
                 .resetElapsedContainerMillis("container closed");
+        mIsOpen = false;
     }
 
     protected abstract void handleClose(boolean animate);
diff --git a/src/com/android/launcher3/FirstFrameAnimatorHelper.java b/src/com/android/launcher3/FirstFrameAnimatorHelper.java
index 4eac4a4..e7ca121 100644
--- a/src/com/android/launcher3/FirstFrameAnimatorHelper.java
+++ b/src/com/android/launcher3/FirstFrameAnimatorHelper.java
@@ -24,7 +24,8 @@
 import android.view.ViewPropertyAnimator;
 import android.view.ViewTreeObserver;
 import com.android.launcher3.util.Thunk;
-import com.android.launcher3.util.TraceHelper;
+
+import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
 
 /*
  *  This is a helper class that listens to updates from the corresponding animation.
@@ -36,7 +37,6 @@
     private static final String TAG = "FirstFrameAnimatorHlpr";
     private static final boolean DEBUG = false;
     private static final int MAX_DELAY = 1000;
-    private static final int IDEAL_FRAME_DURATION = 16;
     private final View mTarget;
     private long mStartFrame;
     private long mStartTime = -1;
@@ -109,9 +109,9 @@
             // prevents a large jump in the animation due to an expensive first frame
             } else if (frameNum == 1 && currentTime < mStartTime + MAX_DELAY &&
                        !mAdjustedSecondFrameTime &&
-                       currentTime > mStartTime + IDEAL_FRAME_DURATION &&
-                       currentPlayTime > IDEAL_FRAME_DURATION) {
-                animation.setCurrentPlayTime(IDEAL_FRAME_DURATION);
+                       currentTime > mStartTime + SINGLE_FRAME_MS &&
+                       currentPlayTime > SINGLE_FRAME_MS) {
+                animation.setCurrentPlayTime(SINGLE_FRAME_MS);
                 mAdjustedSecondFrameTime = true;
             } else {
                 if (frameNum > 1) {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 5e06d92..1285a2b 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -737,6 +737,8 @@
         NotificationListener.removeNotificationsChangedListener();
         getStateManager().moveToRestState();
 
+        // Workaround for b/78520668, explicitly trim memory once UI is hidden
+        UiFactory.onTrimMemory(this, TRIM_MEMORY_UI_HIDDEN);
     }
 
     @Override
@@ -787,7 +789,7 @@
         // Refresh shortcuts if the permission changed.
         mModel.refreshShortcutsIfRequired();
 
-        DiscoveryBounce.showIfNeeded(this);
+        DiscoveryBounce.showForHomeIfNeeded(this);
         if (mLauncherCallbacks != null) {
             mLauncherCallbacks.onResume();
         }
@@ -1278,7 +1280,7 @@
             }
 
             if (mLauncherCallbacks != null) {
-                mLauncherCallbacks.onHomeIntent();
+                mLauncherCallbacks.onHomeIntent(internalStateHandled);
             }
         }
 
diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java
index 35faaea..6aef658 100644
--- a/src/com/android/launcher3/LauncherCallbacks.java
+++ b/src/com/android/launcher3/LauncherCallbacks.java
@@ -50,7 +50,7 @@
     void onAttachedToWindow();
     void onDetachedFromWindow();
     void dump(String prefix, FileDescriptor fd, PrintWriter w, String[] args);
-    void onHomeIntent();
+    void onHomeIntent(boolean internalStateHandled);
     boolean handleBackPressed();
     void onTrimMemory(int level);
 
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index fe953fe..c2416d4 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -1556,11 +1556,6 @@
         return mDownMotionY;
     }
 
-    @Override
-    public boolean onHoverEvent(android.view.MotionEvent event) {
-        return true;
-    }
-
     protected interface ComputePageScrollsLogic {
 
         boolean shouldIncludeView(View view);
diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java
index 024b4eb..7870af9 100644
--- a/src/com/android/launcher3/SecondaryDropTarget.java
+++ b/src/com/android/launcher3/SecondaryDropTarget.java
@@ -52,7 +52,7 @@
 
     private final Alarm mCacheExpireAlarm;
 
-    private int mCurrentAccessibilityAction = -1;
+    protected int mCurrentAccessibilityAction = -1;
     public SecondaryDropTarget(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
@@ -70,7 +70,7 @@
         setupUi(UNINSTALL);
     }
 
-    private void setupUi(int action) {
+    protected void setupUi(int action) {
         if (action == mCurrentAccessibilityAction) {
             return;
         }
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 98440ff..006dc95 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -103,6 +103,8 @@
     public static final boolean ATLEAST_LOLLIPOP_MR1 =
             Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1;
 
+    public static final int SINGLE_FRAME_MS = 16;
+
     /**
      * Indicates if the device has a debug build. Should only be used to store additional info or
      * add extra logging and not for changing the app behavior.
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 7ecafdb..d2a126f 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -477,7 +477,7 @@
         super.onViewAdded(child);
     }
 
-    boolean isTouchActive() {
+    public boolean isTouchActive() {
         return mTouchState != TOUCH_STATE_REST;
     }
 
@@ -974,19 +974,9 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
-        switch (ev.getAction() & MotionEvent.ACTION_MASK) {
-        case MotionEvent.ACTION_DOWN:
+        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
             mXDown = ev.getX();
             mYDown = ev.getY();
-            break;
-        case MotionEvent.ACTION_POINTER_UP:
-        case MotionEvent.ACTION_UP:
-            if (mTouchState == TOUCH_STATE_REST) {
-                final CellLayout currentPage = (CellLayout) getChildAt(mCurrentPage);
-                if (currentPage != null) {
-                    onWallpaperTap(ev);
-                }
-            }
         }
         return super.onInterceptTouchEvent(ev);
     }
@@ -1443,7 +1433,7 @@
         }
     }
 
-    protected void onWallpaperTap(MotionEvent ev) {
+    public void onWallpaperTap(MotionEvent ev) {
         final int[] position = mTempXY;
         getLocationOnScreen(position);
 
diff --git a/src/com/android/launcher3/allapps/DiscoveryBounce.java b/src/com/android/launcher3/allapps/DiscoveryBounce.java
index fddafb2..f73916c 100644
--- a/src/com/android/launcher3/allapps/DiscoveryBounce.java
+++ b/src/com/android/launcher3/allapps/DiscoveryBounce.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,35 +17,44 @@
 package com.android.launcher3.allapps;
 
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
 
 import android.animation.Animator;
 import android.animation.AnimatorInflater;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.Keyframe;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.TimeInterpolator;
 import android.app.ActivityManager;
-import android.content.Context;
+import android.os.Handler;
 import android.view.MotionEvent;
+import android.view.animation.PathInterpolator;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.states.InternalStateHandler;
 
 /**
- * Floating view responsible for showing discovery bounce animation
+ * Abstract base class of floating view responsible for showing discovery bounce animation
  */
 public class DiscoveryBounce extends AbstractFloatingView {
 
-    public static final String APPS_VIEW_SHOWN = "launcher.apps_view_shown";
+    private static final long DELAY_MS = 200;
+
+    public static final String HOME_BOUNCE_SEEN = "launcher.apps_view_shown";
+    public static final String SHELF_BOUNCE_SEEN = "launcher.shelf_bounce_seen";
 
     private final Launcher mLauncher;
     private final Animator mDiscoBounceAnimation;
 
-    public DiscoveryBounce(Launcher launcher) {
+    public DiscoveryBounce(Launcher launcher, Animator animator) {
         super(launcher, null);
         mLauncher = launcher;
 
-        mDiscoBounceAnimation = AnimatorInflater.loadAnimator(mLauncher,
-                R.animator.discovery_bounce);
+        mDiscoBounceAnimation = animator;
         AllAppsTransitionController controller = mLauncher.getAllAppsController();
         mDiscoBounceAnimation.setTarget(controller);
         mDiscoBounceAnimation.addListener(controller.getProgressAnimatorListener());
@@ -96,16 +105,72 @@
         return (type & TYPE_ON_BOARD_POPUP) != 0;
     }
 
-    public static void showIfNeeded(Launcher launcher) {
+    public static void showForHomeIfNeeded(Launcher launcher) {
+        showForHomeIfNeeded(launcher, true);
+    }
+
+    private static void showForHomeIfNeeded(Launcher launcher, boolean withDelay) {
         if (!launcher.isInState(NORMAL)
-                || launcher.getSharedPrefs().getBoolean(APPS_VIEW_SHOWN, false)
+                || launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)
                 || AbstractFloatingView.getTopOpenView(launcher) != null
                 || UserManagerCompat.getInstance(launcher).isDemoUser()
                 || ActivityManager.isRunningInTestHarness()) {
             return;
         }
 
-        DiscoveryBounce view = new DiscoveryBounce(launcher);
+        if (withDelay) {
+            new Handler().postDelayed(() -> showForHomeIfNeeded(launcher, false), DELAY_MS);
+            return;
+        }
+
+        DiscoveryBounce view = new DiscoveryBounce(launcher,
+                AnimatorInflater.loadAnimator(launcher, R.animator.discovery_bounce));
+        view.mIsOpen = true;
+        launcher.getDragLayer().addView(view);
+    }
+
+    public static void showForOverviewIfNeeded(Launcher launcher) {
+        showForOverviewIfNeeded(launcher, true);
+    }
+
+    private static void showForOverviewIfNeeded(Launcher launcher, boolean withDelay) {
+        if (!launcher.isInState(OVERVIEW)
+                || !launcher.hasBeenResumed()
+                || launcher.isForceInvisible()
+                || launcher.getDeviceProfile().isVerticalBarLayout()
+                || launcher.getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false)
+                || UserManagerCompat.getInstance(launcher).isDemoUser()
+                || ActivityManager.isRunningInTestHarness()) {
+            return;
+        }
+
+        if (withDelay) {
+            new Handler().postDelayed(() -> showForOverviewIfNeeded(launcher, false), DELAY_MS);
+            return;
+        } else if (InternalStateHandler.hasPending()
+                || AbstractFloatingView.getTopOpenView(launcher) != null) {
+            // TODO: Move these checks to the top and call this method after invalidate handler.
+            return;
+        }
+
+        float verticalProgress = OVERVIEW.getVerticalProgress(launcher);
+
+        TimeInterpolator pathInterpolator = new PathInterpolator(0.35f, 0, 0.5f, 1);
+        Keyframe keyframe3 = Keyframe.ofFloat(0.423f, verticalProgress - (1 - 0.9438f));
+        keyframe3.setInterpolator(pathInterpolator);
+        Keyframe keyframe4 = Keyframe.ofFloat(0.654f, verticalProgress);
+        keyframe4.setInterpolator(pathInterpolator);
+
+        PropertyValuesHolder propertyValuesHolder = PropertyValuesHolder.ofKeyframe("progress",
+                Keyframe.ofFloat(0, verticalProgress),
+                Keyframe.ofFloat(0.346f, verticalProgress), keyframe3, keyframe4,
+                Keyframe.ofFloat(1f, verticalProgress));
+        ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(null,
+                new PropertyValuesHolder[]{propertyValuesHolder});
+        animator.setDuration(2166);
+        animator.setRepeatCount(5);
+
+        DiscoveryBounce view = new DiscoveryBounce(launcher, animator);
         view.mIsOpen = true;
         launcher.getDragLayer().addView(view);
     }
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 12d7dc7..99c800d 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -39,6 +39,7 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewDebug;
+import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.AnimationUtils;
 import android.view.inputmethod.EditorInfo;
@@ -507,16 +508,9 @@
         // dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice.
         mDeleteFolderOnDropCompleted = false;
 
-        final Runnable onCompleteRunnable;
         centerAboutIcon();
 
         AnimatorSet anim = new FolderAnimationManager(this, true /* isOpening */).getAnimator();
-        onCompleteRunnable = new Runnable() {
-            @Override
-            public void run() {
-                mLauncher.getUserEventDispatcher().resetElapsedContainerMillis("folder opened");
-            }
-        };
         anim.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
@@ -532,7 +526,7 @@
             public void onAnimationEnd(Animator animation) {
                 mState = STATE_OPEN;
 
-                onCompleteRunnable.run();
+                mLauncher.getUserEventDispatcher().resetElapsedContainerMillis("folder opened");
                 mContent.setFocusOnFirstChild();
             }
         });
@@ -614,9 +608,6 @@
             mFolderIcon.clearLeaveBehindIfExists();
         }
 
-        if (!(getParent() instanceof DragLayer)) return;
-        DragLayer parent = (DragLayer) getParent();
-
         if (animate) {
             animateClosed();
         } else {
@@ -625,7 +616,8 @@
 
         // Notify the accessibility manager that this folder "window" has disappeared and no
         // longer occludes the workspace items
-        parent.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+        mLauncher.getDragLayer().sendAccessibilityEvent(
+                AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
     }
 
     private void animateClosed() {
diff --git a/src/com/android/launcher3/states/InternalStateHandler.java b/src/com/android/launcher3/states/InternalStateHandler.java
index 0a2c3e4..cf7c6ba 100644
--- a/src/com/android/launcher3/states/InternalStateHandler.java
+++ b/src/com/android/launcher3/states/InternalStateHandler.java
@@ -60,6 +60,10 @@
         return sScheduler.clearReference(this);
     }
 
+    public static boolean hasPending() {
+        return sScheduler.hasPending();
+    }
+
     public static boolean handleCreate(Launcher launcher, Intent intent) {
         return handleIntent(launcher, intent, false, false);
     }
@@ -132,5 +136,9 @@
             }
             return false;
         }
+
+        public boolean hasPending() {
+            return mPendingHandler.get() != null;
+        }
     }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 4c7ce1f..c0ad110 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.touch;
 
+import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 
 import android.animation.Animator;
@@ -40,7 +41,6 @@
 
     private static final String TAG = "ASCTouchController";
     public static final float RECATCH_REJECTION_FRACTION = .0875f;
-    public static final int SINGLE_FRAME_MS = 16;
 
     // Progress after which the transition is assumed to be a success in case user does not fling
     public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
index 23f55aa..f59f14e 100644
--- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java
+++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
@@ -17,6 +17,7 @@
 
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_POINTER_UP;
 import static android.view.MotionEvent.ACTION_UP;
 import static android.view.ViewConfiguration.getLongPressTimeout;
 
@@ -30,6 +31,7 @@
 import android.view.View.OnTouchListener;
 
 import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Workspace;
@@ -71,8 +73,7 @@
         int action = ev.getActionMasked();
         if (action == ACTION_DOWN) {
             // Check if we can handle long press.
-            boolean handleLongPress = AbstractFloatingView.getTopOpenView(mLauncher) == null
-                    && mLauncher.isInState(NORMAL);
+            boolean handleLongPress = canHandleLongPress();
 
             if (handleLongPress) {
                 // Check if the event is not near the edges
@@ -122,12 +123,28 @@
             // We don't want to handle touch, let workspace handle it as usual.
             result = false;
         }
+
+        if (action == ACTION_UP || action == ACTION_POINTER_UP) {
+            if (!mWorkspace.isTouchActive()) {
+                final CellLayout currentPage =
+                        (CellLayout) mWorkspace.getChildAt(mWorkspace.getCurrentPage());
+                if (currentPage != null) {
+                    mWorkspace.onWallpaperTap(ev);
+                }
+            }
+        }
+
         if (action == ACTION_UP || action == ACTION_CANCEL) {
             cancelLongPress();
         }
         return result;
     }
 
+    private boolean canHandleLongPress() {
+        return AbstractFloatingView.getTopOpenView(mLauncher) == null
+                && mLauncher.isInState(NORMAL);
+    }
+
     private void cancelLongPress() {
         mWorkspace.removeCallbacks(this);
         mLongPressState = STATE_CANCELLED;
@@ -136,15 +153,19 @@
     @Override
     public void run() {
         if (mLongPressState == STATE_REQUESTED) {
-            mLongPressState = STATE_PENDING_PARENT_INFORM;
-            mWorkspace.getParent().requestDisallowInterceptTouchEvent(true);
+            if (canHandleLongPress()) {
+                mLongPressState = STATE_PENDING_PARENT_INFORM;
+                mWorkspace.getParent().requestDisallowInterceptTouchEvent(true);
 
-            mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
-                    HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
-            mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.LONGPRESS,
-                    Action.Direction.NONE, ContainerType.WORKSPACE,
-                    mWorkspace.getCurrentPage());
-            OptionsPopupView.showDefaultOptions(mLauncher, mTouchDownPoint.x, mTouchDownPoint.y);
+                mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
+                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+                mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.LONGPRESS,
+                        Action.Direction.NONE, ContainerType.WORKSPACE,
+                        mWorkspace.getCurrentPage());
+                OptionsPopupView.showDefaultOptions(mLauncher, mTouchDownPoint.x, mTouchDownPoint.y);
+            } else {
+                cancelLongPress();
+            }
         }
     }
 }
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index 489e59e..149b38b 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -34,6 +34,8 @@
 
 import java.util.ArrayList;
 
+import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
+
 /**
  * A viewgroup with utility methods for drag-n-drop and touch interception
  */
@@ -119,6 +121,21 @@
     }
 
     @Override
+    public void onViewRemoved(View child) {
+        super.onViewRemoved(child);
+        if (child instanceof AbstractFloatingView) {
+            // Handles the case where the view is removed without being properly closed.
+            // This can happen if something goes wrong during a state change/transition.
+            postDelayed(() -> {
+                AbstractFloatingView floatingView = (AbstractFloatingView) child;
+                if (floatingView.isOpen()) {
+                    floatingView.close(false);
+                }
+            }, SINGLE_FRAME_MS);
+        }
+    }
+
+    @Override
     public boolean onTouchEvent(MotionEvent ev) {
         int action = ev.getAction();
         if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
diff --git a/src/com/android/launcher3/views/SpringRelativeLayout.java b/src/com/android/launcher3/views/SpringRelativeLayout.java
index 090b3e6..a508191 100644
--- a/src/com/android/launcher3/views/SpringRelativeLayout.java
+++ b/src/com/android/launcher3/views/SpringRelativeLayout.java
@@ -24,7 +24,6 @@
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.RecyclerView.EdgeEffectFactory;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.util.SparseBooleanArray;
 import android.view.View;
 import android.widget.EdgeEffect;
@@ -58,6 +57,7 @@
     private final SpringAnimation mSpring;
 
     private float mDampedScrollShift = 0;
+    private SpringEdgeEffect mActiveEdge;
 
     public SpringRelativeLayout(Context context) {
         this(context, null);
@@ -90,6 +90,13 @@
         return super.drawChild(canvas, child, drawingTime);
     }
 
+    private void setActiveEdge(SpringEdgeEffect edge) {
+        if (mActiveEdge != edge && mActiveEdge != null) {
+            mActiveEdge.mDistance = 0;
+        }
+        mActiveEdge = edge;
+    }
+
     private void setDampedScrollShift(float shift) {
         if (shift != mDampedScrollShift) {
             mDampedScrollShift = shift;
@@ -144,6 +151,7 @@
 
         @Override
         public void onPull(float deltaDistance, float displacement) {
+            setActiveEdge(this);
             mDistance += deltaDistance * (mVelocityMultiplier / 3f);
             setDampedScrollShift(mDistance * getHeight());
         }
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsState.java b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsState.java
index 49a9dc7..6366b2d 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsState.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsState.java
@@ -16,7 +16,7 @@
 package com.android.launcher3.uioverrides;
 
 import static com.android.launcher3.LauncherAnimUtils.ALL_APPS_TRANSITION_MS;
-import static com.android.launcher3.allapps.DiscoveryBounce.APPS_VIEW_SHOWN;
+import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
 
 import android.view.View;
@@ -49,8 +49,8 @@
 
     @Override
     public void onStateEnabled(Launcher launcher) {
-        if (!launcher.getSharedPrefs().getBoolean(APPS_VIEW_SHOWN, false)) {
-            launcher.getSharedPrefs().edit().putBoolean(APPS_VIEW_SHOWN, true).apply();
+        if (!launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)) {
+            launcher.getSharedPrefs().edit().putBoolean(HOME_BOUNCE_SEEN, true).apply();
         }
 
         AbstractFloatingView.closeAllOpenViews(launcher);
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index a54268a..af8b15c 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -18,8 +18,6 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.launcher3.tests">
 
-    <uses-sdk android:targetSdkVersion="25" android:minSdkVersion="21"/>
-
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />