Merge "Not opening current task for closed Recents" into ub-launcher3-master
diff --git a/proguard.flags b/proguard.flags
index cac8930..086337d 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -39,7 +39,7 @@
   public int getY();
 }
 
--keep class com.android.launcher3.dragndrop.DragLayer$LayoutParams {
+-keep class com.android.launcher3.views.BaseDragLayer$LayoutParams {
   public void setWidth(int);
   public int getWidth();
   public void setHeight(int);
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 4a26494..8b28597 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -47,7 +47,8 @@
         <!-- STOPSHIP: Change exported to false once all the integration is complete.
         It is set to true so that the activity can be started from command line -->
         <activity android:name="com.android.quickstep.RecentsActivity"
-            android:exported="true" />
+            android:exported="true"
+            android:excludeFromRecents="true" />
 
         <!-- Content provider to settings search -->
         <provider
diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar
index 33987e2..6e56055 100644
--- a/quickstep/libs/sysui_shared.jar
+++ b/quickstep/libs/sysui_shared.jar
Binary files differ
diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
index 872e6ca..f919339 100644
--- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
@@ -15,27 +15,92 @@
  */
 package com.android.launcher3;
 
+import static com.android.systemui.shared.recents.utilities.Utilities
+        .postAtFrontOfQueueAsynchronously;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.os.Handler;
+import android.support.annotation.BinderThread;
+import android.support.annotation.UiThread;
 
 import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
-import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously;
+@TargetApi(Build.VERSION_CODES.P)
+public abstract class LauncherAnimationRunner extends AnimatorListenerAdapter
+        implements RemoteAnimationRunnerCompat {
 
-public abstract class LauncherAnimationRunner implements RemoteAnimationRunnerCompat {
+    private static final int REFRESH_RATE_MS = 16;
 
-    AnimatorSet mAnimator;
-    private Launcher mLauncher;
+    private final Handler mHandler;
 
-    LauncherAnimationRunner(Launcher launcher) {
-        mLauncher = launcher;
+    private Runnable mSysFinishRunnable;
+
+    private AnimatorSet mAnimator;
+
+    public LauncherAnimationRunner(Handler handler) {
+        mHandler = handler;
     }
 
+    @BinderThread
+    @Override
+    public void onAnimationStart(RemoteAnimationTargetCompat[] targetCompats, Runnable runnable) {
+        postAtFrontOfQueueAsynchronously(mHandler, () -> {
+            // Finish any previous animation
+            finishSystemAnimation();
+
+            mSysFinishRunnable = runnable;
+            mAnimator = getAnimator(targetCompats);
+            if (mAnimator == null) {
+                finishSystemAnimation();
+                return;
+            }
+            mAnimator.addListener(this);
+            mAnimator.start();
+            // 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);
+
+        });
+    }
+
+
+    @UiThread
+    public abstract AnimatorSet getAnimator(RemoteAnimationTargetCompat[] targetCompats);
+
+    @UiThread
+    @Override
+    public void onAnimationEnd(Animator animation) {
+        if (animation == mAnimator) {
+            mAnimator = null;
+            finishSystemAnimation();
+        }
+    }
+
+    /**
+     * Called by the system
+     */
+    @BinderThread
     @Override
     public void onAnimationCancelled() {
-        postAtFrontOfQueueAsynchronously(mLauncher.getWindow().getDecorView().getHandler(), () -> {
+        postAtFrontOfQueueAsynchronously(mHandler, () -> {
             if (mAnimator != null) {
-                mAnimator.cancel();
+                mAnimator.removeListener(this);
+                mAnimator.end();
+                mAnimator = null;
             }
         });
     }
+
+    @UiThread
+    private void finishSystemAnimation() {
+        if (mSysFinishRunnable != null) {
+            mSysFinishRunnable.run();
+            mSysFinishRunnable = null;
+        }
+    }
 }
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index 8e66526..0dbf404 100644
--- a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
 import static com.android.systemui.shared.recents.utilities.Utilities.getNextFrameNumber;
 import static com.android.systemui.shared.recents.utilities.Utilities.getSurface;
-import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
 
@@ -41,6 +40,7 @@
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Handler;
+import android.os.Looper;
 import android.util.Log;
 import android.view.Surface;
 import android.view.View;
@@ -80,7 +80,6 @@
         implements OnDeviceProfileChangeListener {
 
     private static final String TAG = "LauncherTransition";
-    private static final int REFRESH_RATE_MS = 16;
     private static final int STATUS_BAR_TRANSITION_DURATION = 120;
 
     private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION =
@@ -99,7 +98,9 @@
 
     private final DragLayer mDragLayer;
     private final Launcher mLauncher;
-    private DeviceProfile mDeviceProfile;
+
+    private final Handler mHandler;
+    private final boolean mIsRtl;
 
     private final float mContentTransY;
     private final float mWorkspaceTransY;
@@ -107,17 +108,35 @@
     private final float mRecentsTransY;
     private final float mRecentsScale;
 
+    private DeviceProfile mDeviceProfile;
     private View mFloatingView;
-    private boolean mIsRtl;
 
-    private LauncherTransitionAnimator mCurrentAnimator;
+    private final AnimatorListenerAdapter mUiResetListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mDragLayer.setAlpha(1f);
+            mDragLayer.setTranslationY(0f);
+
+            View appsView = mLauncher.getAppsView();
+            appsView.setAlpha(1f);
+            appsView.setTranslationY(0f);
+        }
+    };
+
+    private final AnimatorListenerAdapter mReapplyStateListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mLauncher.getStateManager().reapplyState();
+        }
+    };
 
     public LauncherAppTransitionManagerImpl(Context context) {
         mLauncher = Launcher.getLauncher(context);
         mDragLayer = mLauncher.getDragLayer();
+        mHandler = new Handler(Looper.getMainLooper());
+        mIsRtl = Utilities.isRtl(mLauncher.getResources());
         mDeviceProfile = mLauncher.getDeviceProfile();
 
-        mIsRtl = Utilities.isRtl(mLauncher.getResources());
 
         Resources res = mLauncher.getResources();
         mContentTransY = res.getDimensionPixelSize(R.dimen.content_trans_y);
@@ -135,26 +154,6 @@
         mDeviceProfile = dp;
     }
 
-    private void setCurrentAnimator(LauncherTransitionAnimator animator) {
-        if (isAnimating()) {
-            mCurrentAnimator.cancel();
-        }
-        mCurrentAnimator = animator;
-    }
-
-    @Override
-    public void finishLauncherAnimation() {
-        if (isAnimating()) {
-            mCurrentAnimator.finishLauncherAnimation();
-        }
-        mCurrentAnimator = null;
-    }
-
-    @Override
-    public boolean isAnimating() {
-        return mCurrentAnimator != null && mCurrentAnimator.isRunning();
-    }
-
     /**
      * @return ActivityOptions with remote animations that controls how the window of the opening
      *         targets are displayed.
@@ -162,58 +161,35 @@
     @Override
     public ActivityOptions getActivityLaunchOptions(Launcher launcher, View v) {
         if (hasControlRemoteAppTransitionPermission()) {
-            TaskView taskView = findTaskViewToLaunch(launcher, v);
             try {
-                RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mLauncher) {
-                    @Override
-                    public void onAnimationStart(RemoteAnimationTargetCompat[] targets,
-                                                 Runnable finishedCallback) {
-                        // Post at front of queue ignoring sync barriers to make sure it gets
-                        // processed before the next frame.
-                        postAtFrontOfQueueAsynchronously(v.getHandler(), () -> {
-                            final boolean removeTrackingView;
-                            LauncherTransitionAnimator animator = composeRecentsLaunchAnimator(
-                                    taskView == null ? v : taskView, targets);
-                            if (animator != null) {
-                                // We are animating the task view directly, do not remove it after
-                                removeTrackingView = false;
-                            } else {
-                                animator = composeAppLaunchAnimator(v, targets);
-                                // A new floating view is created for the animation, remove it after
-                                removeTrackingView = true;
-                            }
+                RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mHandler) {
 
-                            setCurrentAnimator(animator);
-                            mAnimator = animator.getAnimatorSet();
-                            mAnimator.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public AnimatorSet getAnimator(RemoteAnimationTargetCompat[] targetCompats) {
+                        Animator[] anims = composeRecentsLaunchAnimator(v, targetCompats);
+                        AnimatorSet anim = new AnimatorSet();
+                        if (anims != null) {
+                            anim.playTogether(anims);
+                        } else {
+                            anim.play(getLauncherAnimators(v, targetCompats));
+                            anim.play(getWindowAnimators(v, targetCompats));
+                            anim.addListener(new AnimatorListenerAdapter() {
                                 @Override
                                 public void onAnimationEnd(Animator animation) {
                                     // Reset launcher to normal state
                                     v.setVisibility(View.VISIBLE);
-                                    if (removeTrackingView) {
-                                        ((ViewGroup) mDragLayer.getParent()).removeView(
-                                                mFloatingView);
-                                    }
-
-                                    mDragLayer.setAlpha(1f);
-                                    mDragLayer.setTranslationY(0f);
-
-                                    View appsView = mLauncher.getAppsView();
-                                    appsView.setAlpha(1f);
-                                    appsView.setTranslationY(0f);
-
-                                    finishedCallback.run();
+                                    ((ViewGroup) mDragLayer.getParent()).removeView(mFloatingView);
                                 }
                             });
-                            mAnimator.start();
-                            // 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);
-                        });
+                            anim.addListener(mUiResetListener);
+                        }
+                        mLauncher.getStateManager().setCurrentAnimation(anim);
+                        return anim;
                     }
                 };
 
-                int duration = taskView != null ? RECENTS_LAUNCH_DURATION : APP_LAUNCH_DURATION;
+                int duration = findTaskViewToLaunch(launcher, v, null) != null
+                        ? RECENTS_LAUNCH_DURATION : APP_LAUNCH_DURATION;
                 int statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION;
                 return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat(
                         runner, duration, statusBarTransitionDelay));
@@ -232,18 +208,19 @@
      * Otherwise, we will assume we are using a normal app transition, but it's possible that the
      * opening remote target (which we don't get until onAnimationStart) will resolve to a TaskView.
      */
-    private TaskView findTaskViewToLaunch(Launcher launcher, View v) {
+    private TaskView findTaskViewToLaunch(
+            BaseDraggingActivity activity, View v, RemoteAnimationTargetCompat[] targets) {
         if (v instanceof TaskView) {
             return (TaskView) v;
         }
-        if (!launcher.isInState(LauncherState.OVERVIEW)) {
-            return null;
-        }
+        RecentsView recentsView = activity.getOverviewPanel();
+
+        // It's possible that the launched view can still be resolved to a visible task view, check
+        // the task id of the opening task and see if we can find a match.
         if (v.getTag() instanceof ItemInfo) {
             ItemInfo itemInfo = (ItemInfo) v.getTag();
             ComponentName componentName = itemInfo.getTargetComponent();
             if (componentName != null) {
-                RecentsView recentsView = launcher.getOverviewPanel();
                 for (int i = 0; i < recentsView.getChildCount(); i++) {
                     TaskView taskView = (TaskView) recentsView.getPageAt(i);
                     if (recentsView.isTaskViewVisible(taskView)) {
@@ -255,32 +232,10 @@
                 }
             }
         }
-        return null;
-    }
 
-    /**
-     * Composes the animations for a launch from the recents list if possible.
-     */
-    private LauncherTransitionAnimator composeRecentsLaunchAnimator(View v,
-            RemoteAnimationTargetCompat[] targets) {
-        RecentsView recentsView = mLauncher.getOverviewPanel();
-        boolean launcherClosing = launcherIsATargetWithMode(targets, MODE_CLOSING);
-        MutableBoolean skipLauncherChanges = new MutableBoolean(!launcherClosing);
-        if (v instanceof TaskView) {
-            // We already found a task view to launch, so use that for the animation.
-            TaskView taskView = (TaskView) v;
-            return new LauncherTransitionAnimator(getRecentsLauncherAnimator(recentsView, taskView),
-                    getRecentsWindowAnimator(taskView, skipLauncherChanges, targets));
-        }
-
-        // It's possible that the launched view can still be resolved to a visible task view, check
-        // the task id of the opening task and see if we can find a match.
-
-        // Ensure recents is actually visible
-        if (!mLauncher.getStateManager().getState().overviewUi) {
+        if (targets == null) {
             return null;
         }
-
         // Resolve the opening task id
         int openingTaskId = -1;
         for (RemoteAnimationTargetCompat target : targets) {
@@ -301,19 +256,35 @@
         if (taskView == null || !recentsView.isTaskViewVisible(taskView)) {
             return null;
         }
+        return taskView;
+    }
+
+    /**
+     * Composes the animations for a launch from the recents list if possible.
+     */
+    private Animator[] composeRecentsLaunchAnimator(View v,
+            RemoteAnimationTargetCompat[] targets) {
+        // Ensure recents is actually visible
+        if (!mLauncher.getStateManager().getState().overviewUi) {
+            return null;
+        }
+
+        RecentsView recentsView = mLauncher.getOverviewPanel();
+        boolean launcherClosing = launcherIsATargetWithMode(targets, MODE_CLOSING);
+        boolean skipLauncherChanges = !launcherClosing;
+
+        TaskView taskView = findTaskViewToLaunch(mLauncher, v, targets);
+        if (taskView == null) {
+            return null;
+        }
 
         // Found a visible recents task that matches the opening app, lets launch the app from there
         Animator launcherAnim;
-        AnimatorListenerAdapter windowAnimEndListener;
+        final AnimatorListenerAdapter windowAnimEndListener;
         if (launcherClosing) {
             launcherAnim = getRecentsLauncherAnimator(recentsView, taskView);
-            windowAnimEndListener = new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    // Make sure recents gets fixed up by resetting task alphas and scales, etc.
-                    mLauncher.getStateManager().reapplyState();
-                }
-            };
+            // Make sure recents gets fixed up by resetting task alphas and scales, etc.
+            windowAnimEndListener = mReapplyStateListener;
         } else {
             AnimatorPlaybackController controller =
                     mLauncher.getStateManager()
@@ -330,7 +301,7 @@
 
         Animator windowAnim = getRecentsWindowAnimator(taskView, skipLauncherChanges, targets);
         windowAnim.addListener(windowAnimEndListener);
-        return new LauncherTransitionAnimator(launcherAnim, windowAnim, skipLauncherChanges);
+        return new Animator[] {launcherAnim, windowAnim};
     }
 
     /**
@@ -418,7 +389,7 @@
      * @return Animator that controls the window of the opening targets for the recents launch
      * animation.
      */
-    private ValueAnimator getRecentsWindowAnimator(TaskView v, MutableBoolean skipLauncherChanges,
+    private ValueAnimator getRecentsWindowAnimator(TaskView v, boolean skipLauncherChanges,
             RemoteAnimationTargetCompat[] targets) {
         Rect taskViewBounds = new Rect();
         mDragLayer.getDescendantRectRelativeToSelf(v, taskViewBounds);
@@ -454,7 +425,7 @@
                 final float percent = animation.getAnimatedFraction();
                 TaskWindowBounds tw = recentsInterpolator.interpolate(percent);
 
-                if (!skipLauncherChanges.value) {
+                if (!skipLauncherChanges) {
                     v.setScaleX(tw.taskScale);
                     v.setScaleY(tw.taskScale);
                     v.setTranslationX(tw.taskX);
@@ -469,9 +440,8 @@
                 crop.set(tw.winCrop);
 
                 // Fade in the app window.
-                float alphaDelay = 0;
                 float alphaDuration = 75;
-                float alpha = getValue(0f, 1f, alphaDelay, alphaDuration,
+                float alpha = getValue(0f, 1f, 0, alphaDuration,
                         appAnimator.getDuration() * percent, Interpolators.LINEAR);
 
                 TransactionCompat t = new TransactionCompat();
@@ -485,7 +455,7 @@
                         t.setMatrix(target.leash, matrix);
                         t.setWindowCrop(target.leash, crop);
 
-                        if (!skipLauncherChanges.value) {
+                        if (!skipLauncherChanges) {
                             t.deferTransactionUntil(target.leash, surface, frameNumber);
                         }
                     }
@@ -503,15 +473,6 @@
     }
 
     /**
-     * Composes the animations for a launch from an app icon.
-     */
-    private LauncherTransitionAnimator composeAppLaunchAnimator(View v,
-            RemoteAnimationTargetCompat[] targets) {
-        return new LauncherTransitionAnimator(getLauncherAnimators(v, targets),
-                getWindowAnimators(v, targets));
-    }
-
-    /**
      * @return Animators that control the movements of the Launcher and icon of the opening target.
      */
     private AnimatorSet getLauncherAnimators(View v, RemoteAnimationTargetCompat[] targets) {
@@ -595,10 +556,8 @@
         } else {
             mDragLayer.getDescendantRectRelativeToSelf(v, rect);
         }
-        final int viewLocationStart = mIsRtl
-                ? mDeviceProfile.widthPx - rect.right
-                : rect.left;
-        final int viewLocationTop = rect.top;
+        int viewLocationLeft = rect.left;
+        int viewLocationTop = rect.top;
 
         float startScale = 1f;
         if (isBubbleTextView && !isDeepShortcutTextView) {
@@ -611,12 +570,24 @@
         } else {
             rect.set(0, 0, rect.width(), rect.height());
         }
+        viewLocationLeft += rect.left;
+        viewLocationTop += rect.top;
+        int viewLocationStart = mIsRtl
+                ? mDeviceProfile.widthPx - rect.right
+                : viewLocationLeft;
         LayoutParams lp = new LayoutParams(rect.width(), rect.height());
         lp.ignoreInsets = true;
-        lp.setMarginStart(viewLocationStart + rect.left);
-        lp.topMargin = viewLocationTop + rect.top;
+        lp.setMarginStart(viewLocationStart);
+        lp.topMargin = viewLocationTop;
         mFloatingView.setLayoutParams(lp);
 
+        // Set the properties here already to make sure they'are available when running the first
+        // animation frame.
+        mFloatingView.setLeft(viewLocationLeft);
+        mFloatingView.setTop(viewLocationTop);
+        mFloatingView.setRight(viewLocationLeft + rect.width());
+        mFloatingView.setBottom(viewLocationTop + rect.height());
+
         // Swap the two views in place.
         ((ViewGroup) mDragLayer.getParent()).addView(mFloatingView);
         v.setVisibility(View.INVISIBLE);
@@ -726,9 +697,8 @@
                 matrix.postTranslate(transX0, transY0);
 
                 // Fade in the app window.
-                float alphaDelay = 0;
                 float alphaDuration = 60;
-                float alpha = getValue(0f, 1f, alphaDelay, alphaDuration,
+                float alpha = getValue(0f, 1f, 0, alphaDuration,
                         appAnimator.getDuration() * percent, Interpolators.LINEAR);
 
                 // Animate the window crop so that it starts off as a square, and then reveals
@@ -801,42 +771,25 @@
      *         ie. pressing home, swiping up from nav bar.
      */
     private RemoteAnimationRunnerCompat getWallpaperOpenRunner() {
-        return new LauncherAnimationRunner(mLauncher) {
+        return new LauncherAnimationRunner(mHandler) {
             @Override
-            public void onAnimationStart(RemoteAnimationTargetCompat[] targets,
-                                         Runnable finishedCallback) {
-                Handler handler = mLauncher.getWindow().getDecorView().getHandler();
-                postAtFrontOfQueueAsynchronously(handler, () -> {
-                    if (mLauncher.getStateManager().getState().overviewUi) {
-                        // We use a separate transition for Overview mode.
-                        setCurrentAnimator(null);
-                        finishedCallback.run();
-                        return;
-                    }
+            public AnimatorSet getAnimator(RemoteAnimationTargetCompat[] targetCompats) {
+                if (mLauncher.getStateManager().getState().overviewUi) {
+                    // We use a separate transition for Overview mode.
+                    return null;
+                }
 
-                    // We can skip the Launcher content animation in cases where Launcher is not in
-                    // the set of opening targets. This can happen when Launcher is already visible.
-                    // ie. closing a dialog. We still need to animate the window though.
-                    LauncherTransitionAnimator animator = new LauncherTransitionAnimator(
-                            launcherIsATargetWithMode(targets, MODE_OPENING)
-                                    ? getLauncherResumeAnimation()
-                                    : null,
-                            getClosingWindowAnimators(targets));
+                AnimatorSet anim = new AnimatorSet();
+                anim.play(getClosingWindowAnimators(targetCompats));
 
-                    setCurrentAnimator(animator);
-                    mAnimator = animator.getAnimatorSet();
-                    mAnimator.addListener(new AnimatorListenerAdapter() {
-                        @Override
-                        public void onAnimationEnd(Animator animation) {
-                            finishedCallback.run();
-                        }
-                    });
-                    mAnimator.start();
+                if (launcherIsATargetWithMode(targetCompats, MODE_OPENING)) {
+                    AnimatorSet contentAnimation = getLauncherResumeAnimation();
+                    anim.play(contentAnimation);
 
-                    // 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);
-                });
+                    // Only register the content animation for cancellation when state changes
+                    mLauncher.getStateManager().setCurrentAnimation(contentAnimation);
+                }
+                return anim;
             }
         };
     }
@@ -903,16 +856,22 @@
         if (mLauncher.isInState(LauncherState.ALL_APPS)
                 || mLauncher.getDeviceProfile().isVerticalBarLayout()) {
             AnimatorSet contentAnimator = getLauncherContentAnimator(true /* show */);
+            contentAnimator.addListener(mUiResetListener);
             contentAnimator.setStartDelay(LAUNCHER_RESUME_START_DELAY);
             return contentAnimator;
         } else {
             AnimatorSet workspaceAnimator = new AnimatorSet();
+
             mLauncher.getWorkspace().setTranslationY(mWorkspaceTransY);
-            mLauncher.getWorkspace().setAlpha(0f);
             workspaceAnimator.play(ObjectAnimator.ofFloat(mLauncher.getWorkspace(),
                     View.TRANSLATION_Y, mWorkspaceTransY, 0));
-            workspaceAnimator.play(ObjectAnimator.ofFloat(mLauncher.getWorkspace(), View.ALPHA,
-                    0, 1f));
+
+            View currentPage = ((CellLayout) mLauncher.getWorkspace()
+                    .getChildAt(mLauncher.getWorkspace().getCurrentPage()))
+                    .getShortcutsAndWidgets();
+            currentPage.setAlpha(0f);
+            workspaceAnimator.play(ObjectAnimator.ofFloat(currentPage, View.ALPHA, 0, 1f));
+
             workspaceAnimator.setStartDelay(LAUNCHER_RESUME_START_DELAY);
             workspaceAnimator.setDuration(333);
             workspaceAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
@@ -940,6 +899,8 @@
             AnimatorSet resumeLauncherAnimation = new AnimatorSet();
             resumeLauncherAnimation.play(workspaceAnimator);
             resumeLauncherAnimation.playSequentially(allAppsSlideIn, allAppsOvershoot);
+
+            resumeLauncherAnimation.addListener(mReapplyStateListener);
             return resumeLauncherAnimation;
         }
     }
diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java
new file mode 100644
index 0000000..0d1038a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java
@@ -0,0 +1,54 @@
+/*
+ * 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.launcher3;
+
+import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import com.android.launcher3.states.InternalStateHandler;
+import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
+
+import java.util.function.BiPredicate;
+
+@TargetApi(Build.VERSION_CODES.P)
+public class LauncherInitListener extends InternalStateHandler implements ActivityInitListener {
+
+    private final BiPredicate<Launcher, Boolean> mOnInitListener;
+
+    public LauncherInitListener(BiPredicate<Launcher, Boolean> onInitListener) {
+        mOnInitListener = onInitListener;
+    }
+
+    @Override
+    protected boolean init(Launcher launcher, boolean alreadyOnHome) {
+        // For the duration of the gesture, lock the screen orientation to ensure that we do not
+        // rotate mid-quickscrub
+        launcher.getRotationHelper().setStateHandlerRequest(REQUEST_LOCK);
+        return mOnInitListener.test(launcher, alreadyOnHome);
+    }
+
+    @Override
+    public void register() {
+        initWhenReady();
+    }
+
+    @Override
+    public void unregister() {
+        clearReference();
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java b/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java
deleted file mode 100644
index ab9234b..0000000
--- a/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-
-/**
- * Creates an AnimatorSet consisting on one Animator for Launcher transition, and one Animator for
- * the Window transitions.
- *
- * Allows for ending the Launcher animator without ending the Window animator.
- */
-public class LauncherTransitionAnimator {
-
-    private final MutableBoolean mLauncherAnimCancelState;
-
-    private AnimatorSet mAnimatorSet;
-    private Animator mLauncherAnimator;
-    private Animator mWindowAnimator;
-
-    LauncherTransitionAnimator(Animator launcherAnimator, Animator windowAnimator) {
-        this(launcherAnimator, windowAnimator, new MutableBoolean(false));
-    }
-
-
-    LauncherTransitionAnimator(Animator launcherAnimator, Animator windowAnimator,
-            MutableBoolean launcherAnimCancelState) {
-        mLauncherAnimCancelState = launcherAnimCancelState;
-        if (launcherAnimator != null) {
-            mLauncherAnimator = launcherAnimator;
-        }
-        mWindowAnimator = windowAnimator;
-
-        mAnimatorSet = new AnimatorSet();
-        if (launcherAnimator != null) {
-            mAnimatorSet.play(launcherAnimator);
-        }
-        mAnimatorSet.play(windowAnimator);
-    }
-
-    public AnimatorSet getAnimatorSet() {
-        return mAnimatorSet;
-    }
-
-    public void cancel() {
-        mAnimatorSet.cancel();
-        mLauncherAnimCancelState.value = true;
-    }
-
-    public boolean isRunning() {
-        return mAnimatorSet.isRunning();
-    }
-
-    public void finishLauncherAnimation() {
-        if (mLauncherAnimator != null) {
-            mLauncherAnimCancelState.value = true;
-            mLauncherAnimator.end();
-        }
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/MutableBoolean.java b/quickstep/src/com/android/launcher3/MutableBoolean.java
deleted file mode 100644
index 7538217..0000000
--- a/quickstep/src/com/android/launcher3/MutableBoolean.java
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3;
-
-public class MutableBoolean {
-    public boolean value;
-
-    public MutableBoolean(boolean value) {
-        this.value = value;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
index efa83e4..1fb3584 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
@@ -85,8 +85,8 @@
     }
 
     @Override
-    public float getHoseatAlpha(Launcher launcher) {
-        return 0;
+    public int getVisibleElements(Launcher launcher) {
+        return ALL_APPS_HEADER | ALL_APPS_CONTENT;
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DragPauseDetector.java b/quickstep/src/com/android/launcher3/uioverrides/DragPauseDetector.java
deleted file mode 100644
index 6df1aba..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/DragPauseDetector.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2017 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.launcher3.uioverrides;
-
-import com.android.launcher3.Alarm;
-import com.android.launcher3.OnAlarmListener;
-
-/**
- * Utility class to detect a pause during a drag.
- */
-public class DragPauseDetector implements OnAlarmListener {
-
-    private static final float MAX_VELOCITY_TO_PAUSE = 0.2f;
-    private static final long PAUSE_DURATION = 100;
-
-    private final Alarm mAlarm;
-    private final Runnable mOnPauseCallback;
-
-    private boolean mTriggered = false;
-    private int mDisabledFlags = 0;
-
-    public DragPauseDetector(Runnable onPauseCallback) {
-        mOnPauseCallback = onPauseCallback;
-
-        mAlarm = new Alarm();
-        mAlarm.setOnAlarmListener(this);
-        mAlarm.setAlarm(PAUSE_DURATION);
-    }
-
-    public void onDrag(float velocity) {
-        if (mTriggered || !isEnabled()) {
-            return;
-        }
-
-        if (Math.abs(velocity) > MAX_VELOCITY_TO_PAUSE) {
-            // Cancel any previous alarm and set a new alarm
-            mAlarm.setAlarm(PAUSE_DURATION);
-        }
-    }
-
-    @Override
-    public void onAlarm(Alarm alarm) {
-        if (!mTriggered && isEnabled()) {
-            mTriggered = true;
-            mOnPauseCallback.run();
-        }
-    }
-
-    public boolean isTriggered () {
-        return mTriggered;
-    }
-
-    public boolean isEnabled() {
-        return mDisabledFlags == 0;
-    }
-
-    public void addDisabledFlags(int flags) {
-        boolean wasEnabled = isEnabled();
-        mDisabledFlags |= flags;
-        resetAlarm(wasEnabled);
-    }
-
-    public void clearDisabledFlags(int flags) {
-        boolean wasEnabled = isEnabled();
-        mDisabledFlags  &= ~flags;
-        resetAlarm(wasEnabled);
-    }
-
-    private void resetAlarm(boolean wasEnabled) {
-        boolean isEnabled = isEnabled();
-        if (wasEnabled == isEnabled) {
-          // Nothing has changed
-        } if (isEnabled && !mTriggered) {
-            mAlarm.setAlarm(PAUSE_DURATION);
-        } else if (!isEnabled) {
-            mAlarm.cancelAlarm();
-        }
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/EdgeSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/EdgeSwipeController.java
deleted file mode 100644
index 97ac3e6..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/EdgeSwipeController.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.uioverrides;
-
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.touch.SwipeDetector.DIRECTION_NEGATIVE;
-import static com.android.launcher3.touch.SwipeDetector.DIRECTION_POSITIVE;
-import static com.android.launcher3.touch.SwipeDetector.HORIZONTAL;
-import static com.android.launcher3.touch.SwipeDetector.VERTICAL;
-import static com.android.quickstep.TouchInteractionService.EDGE_NAV_BAR;
-
-import android.graphics.Rect;
-import android.metrics.LogMaker;
-import android.view.MotionEvent;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.util.VerticalSwipeController;
-import com.android.quickstep.views.RecentsView;
-
-class EventLogTags {
-    private EventLogTags() {
-    }  // don't instantiate
-
-    /** 524292 sysui_multi_action (content|4) */
-    public static final int SYSUI_MULTI_ACTION = 524292;
-
-    public static void writeSysuiMultiAction(Object[] content) {
-        android.util.EventLog.writeEvent(SYSUI_MULTI_ACTION, content);
-    }
-}
-
-class MetricsLogger {
-    private static MetricsLogger sMetricsLogger;
-
-    private static MetricsLogger getLogger() {
-        if (sMetricsLogger == null) {
-            sMetricsLogger = new MetricsLogger();
-        }
-        return sMetricsLogger;
-    }
-
-    protected void saveLog(Object[] rep) {
-        EventLogTags.writeSysuiMultiAction(rep);
-    }
-
-    public void write(LogMaker content) {
-        if (content.getType() == 0/*MetricsEvent.TYPE_UNKNOWN*/) {
-            content.setType(4/*MetricsEvent.TYPE_ACTION*/);
-        }
-        saveLog(content.serialize());
-    }
-}
-
-/**
- * Extension of {@link VerticalSwipeController} to go from NORMAL to OVERVIEW.
- */
-public class EdgeSwipeController extends VerticalSwipeController implements
-        OnDeviceProfileChangeListener {
-
-    private static final Rect sTempRect = new Rect();
-
-    private final MetricsLogger mMetricsLogger = new MetricsLogger();
-
-    public EdgeSwipeController(Launcher l) {
-        super(l, NORMAL, OVERVIEW, l.getDeviceProfile().isVerticalBarLayout()
-                ? HORIZONTAL : VERTICAL);
-        l.addOnDeviceProfileChangeListener(this);
-    }
-
-    @Override
-    public void onDeviceProfileChanged(DeviceProfile dp) {
-        mDetector.updateDirection(dp.isVerticalBarLayout() ? HORIZONTAL : VERTICAL);
-    }
-
-    @Override
-    protected boolean shouldInterceptTouch(MotionEvent ev) {
-        return mLauncher.isInState(NORMAL) && (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
-    }
-
-    @Override
-    protected int getSwipeDirection(MotionEvent ev) {
-        return isTransitionFlipped() ? DIRECTION_NEGATIVE : DIRECTION_POSITIVE;
-    }
-
-    public EdgeSwipeController(Launcher l, LauncherState baseState) {
-        super(l, baseState);
-    }
-
-    @Override
-    protected boolean isTransitionFlipped() {
-        return mLauncher.getDeviceProfile().isSeascape();
-    }
-
-    @Override
-    protected void onTransitionComplete(boolean wasFling, boolean stateChanged) {
-        if (stateChanged && mToState instanceof OverviewState) {
-            // Mimic ActivityMetricsLogger.logAppTransitionMultiEvents() logging for
-            // "Recents" activity for app transition tests.
-            final LogMaker builder = new LogMaker(761/*APP_TRANSITION*/);
-            builder.setPackageName("com.android.systemui");
-            builder.addTaggedData(871/*FIELD_CLASS_NAME*/,
-                    "com.android.systemui.recents.RecentsActivity");
-            builder.addTaggedData(319/*APP_TRANSITION_DELAY_MS*/,
-                    0/* zero time */);
-            mMetricsLogger.write(builder);
-
-            // Add user event logging for launcher pipeline
-            int direction = Direction.UP;
-            if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
-                direction = Direction.LEFT;
-                if (mLauncher.getDeviceProfile().isSeascape()) {
-                    direction = Direction.RIGHT;
-                }
-            }
-            mLauncher.getUserEventDispatcher().logStateChangeAction(
-                    wasFling ? Touch.FLING : Touch.SWIPE, direction,
-                    ContainerType.NAVBAR, ContainerType.WORKSPACE, // src target
-                    ContainerType.TASKSWITCHER,                    // dst target
-                    mLauncher.getWorkspace().getCurrentPage());
-        }
-    }
-
-    @Override
-    protected float getShiftRange() {
-        return getShiftRange(mLauncher);
-    }
-
-    public static float getShiftRange(Launcher launcher) {
-        RecentsView.getPageRect(launcher.getDeviceProfile(), launcher, sTempRect);
-        DragLayer dl = launcher.getDragLayer();
-        Rect insets = dl.getInsets();
-        DeviceProfile dp = launcher.getDeviceProfile();
-
-        if (dp.isVerticalBarLayout()) {
-            if (dp.isSeascape()) {
-                return insets.left + sTempRect.left;
-            } else {
-                return dl.getWidth() - sTempRect.right + insets.right;
-            }
-        } else {
-            return dl.getHeight() - sTempRect.bottom + insets.bottom;
-        }
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/FastOverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/FastOverviewState.java
index 9541d0d..de056a7 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/FastOverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/FastOverviewState.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.uioverrides;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
 import com.android.quickstep.QuickScrubController;
 import com.android.quickstep.views.RecentsView;
@@ -40,11 +41,16 @@
         recentsView.getQuickScrubController().onFinishedTransitionToQuickScrub();
     }
 
+    public void onStateEnabled(Launcher launcher) {
+        super.onStateEnabled(launcher);
+        AbstractFloatingView.closeAllOpenViews(launcher);
+    }
+
     @Override
-    public float getHoseatAlpha(Launcher launcher) {
+    public int getVisibleElements(Launcher launcher) {
         if (DEBUG_DIFFERENT_UI) {
-            return 0;
+            return NONE;
         }
-        return super.getHoseatAlpha(launcher);
+        return super.getVisibleElements(launcher);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/LandscapeEdgeSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/LandscapeEdgeSwipeController.java
new file mode 100644
index 0000000..23add95
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/LandscapeEdgeSwipeController.java
@@ -0,0 +1,71 @@
+package com.android.launcher3.uioverrides;
+
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.quickstep.TouchInteractionService.EDGE_NAV_BAR;
+
+import android.view.MotionEvent;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.touch.AbstractStateChangeTouchController;
+import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
+import com.android.quickstep.util.SysuiEventLogger;
+
+/**
+ * Touch controller for handling edge swipes in landscape/seascape UI
+ */
+public class LandscapeEdgeSwipeController extends AbstractStateChangeTouchController {
+
+    public LandscapeEdgeSwipeController(Launcher l) {
+        super(l, SwipeDetector.HORIZONTAL);
+    }
+
+    @Override
+    protected boolean canInterceptTouch(MotionEvent ev) {
+        if (mCurrentAnimation != null) {
+            // If we are already animating from a previous state, we can intercept.
+            return true;
+        }
+        if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
+            return false;
+        }
+        return mLauncher.isInState(NORMAL) && (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
+    }
+
+    @Override
+    protected int getSwipeDirection(MotionEvent ev) {
+        mFromState = NORMAL;
+        mToState = OVERVIEW;
+        return SwipeDetector.DIRECTION_BOTH;
+    }
+
+    @Override
+    protected float getShiftRange() {
+        return mLauncher.getDragLayer().getWidth();
+    }
+
+    @Override
+    protected float initCurrentAnimation() {
+        float range = getShiftRange();
+        long maxAccuracy = (long) (2 * range);
+        mCurrentAnimation = mLauncher.getStateManager()
+                .createAnimationToNewWorkspace(mToState, maxAccuracy);
+        return (mLauncher.getDeviceProfile().isSeascape() ? 2 : -2) / range;
+    }
+
+    @Override
+    protected int getDirectionForLog() {
+        return mLauncher.getDeviceProfile().isSeascape() ? Direction.RIGHT : Direction.LEFT;
+    }
+
+    @Override
+    protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
+        super.onSwipeInteractionCompleted(targetState, logAction);
+        if (mFromState == NORMAL && targetState == OVERVIEW) {
+            SysuiEventLogger.writeDummyRecentsTransition(0);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/LandscapeStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/LandscapeStatesTouchController.java
new file mode 100644
index 0000000..720b20a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/LandscapeStatesTouchController.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017 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.launcher3.uioverrides;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+
+import android.view.MotionEvent;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.quickstep.TouchInteractionService;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * Touch controller from going from OVERVIEW to ALL_APPS
+ */
+public class LandscapeStatesTouchController extends PortraitStatesTouchController {
+
+    public LandscapeStatesTouchController(Launcher l) {
+        super(l);
+    }
+
+    @Override
+    protected boolean canInterceptTouch(MotionEvent ev) {
+        if (mCurrentAnimation != null) {
+            // If we are already animating from a previous state, we can intercept.
+            return true;
+        }
+        if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
+            return false;
+        }
+        if (mLauncher.isInState(ALL_APPS)) {
+            // In all-apps only listen if the container cannot scroll itself
+            return mLauncher.getAppsView().shouldContainerScroll(ev);
+        } else if (mLauncher.isInState(NORMAL)) {
+            return true;
+        } else if (mLauncher.isInState(OVERVIEW)) {
+            RecentsView rv = mLauncher.getOverviewPanel();
+            return ev.getY() > (rv.getBottom() - rv.getPaddingBottom());
+        } else {
+            return false;
+        }
+    }
+
+    protected LauncherState getTargetState() {
+        if (mLauncher.isInState(ALL_APPS)) {
+            // Should swipe down go to OVERVIEW instead?
+            return TouchInteractionService.isConnected() ?
+                    mLauncher.getStateManager().getLastState() : NORMAL;
+        } else {
+            return ALL_APPS;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
index 09acb1d..d123dce 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
@@ -22,6 +22,7 @@
 import android.graphics.Rect;
 import android.view.View;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Workspace;
@@ -105,4 +106,29 @@
 
         return new float[] {scale, 0, translationY};
     }
+
+    @Override
+    public int getVisibleElements(Launcher launcher) {
+        if (launcher.getDeviceProfile().isVerticalBarLayout()) {
+            // TODO: Remove hotseat from overview
+            return HOTSEAT;
+        } else {
+            return launcher.getAppsView().getFloatingHeaderView().hasVisibleContent()
+                    ? ALL_APPS_HEADER : HOTSEAT;
+        }
+    }
+
+    @Override
+    public float getVerticalProgress(Launcher launcher) {
+        if (getVisibleElements(launcher) == HOTSEAT) {
+            return super.getVerticalProgress(launcher);
+        }
+        return 1 - (getDefaultSwipeHeight(launcher)
+                / launcher.getAllAppsController().getShiftRange());
+    }
+
+    public static float getDefaultSwipeHeight(Launcher launcher) {
+        DeviceProfile dp = launcher.getDeviceProfile();
+        return dp.allAppsCellHeightPx - dp.allAppsIconTextSizePx;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeUpController.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeUpController.java
deleted file mode 100644
index 4fb3886..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeUpController.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2017 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.launcher3.uioverrides;
-
-import static com.android.launcher3.LauncherState.OVERVIEW;
-
-import android.view.MotionEvent;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.touch.SwipeDetector;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.util.VerticalSwipeController;
-
-/**
- * Extension of {@link VerticalSwipeController} which allows swipe up from OVERVIEW to ALL_APPS
- * Note that the swipe down is handled by {@link TwoStepSwipeController}.
- */
-public class OverviewSwipeUpController extends VerticalSwipeController {
-
-    public OverviewSwipeUpController(Launcher l) {
-        super(l, OVERVIEW);
-    }
-
-    @Override
-    protected boolean shouldInterceptTouch(MotionEvent ev) {
-        if (!mLauncher.isInState(OVERVIEW)) {
-            return false;
-        }
-        if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
-            return ev.getY() >
-                    mLauncher.getDragLayer().getHeight() * OVERVIEW.getVerticalProgress(mLauncher);
-        } else {
-            return mLauncher.getDragLayer().isEventOverHotseat(ev);
-        }
-    }
-
-    @Override
-    protected int getSwipeDirection(MotionEvent ev) {
-        return SwipeDetector.DIRECTION_POSITIVE;
-    }
-
-    @Override
-    protected void onTransitionComplete(boolean wasFling, boolean stateChanged) {
-        if (stateChanged) {
-            // Transition complete. log the action
-            mLauncher.getUserEventDispatcher().logStateChangeAction(
-                    wasFling ? Touch.FLING : Touch.SWIPE,
-                    Direction.UP,
-                    ContainerType.HOTSEAT,
-                    ContainerType.TASKSWITCHER,
-                    ContainerType.ALLAPPS,
-                    mLauncher.getWorkspace().getCurrentPage());
-        }
-
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
new file mode 100644
index 0000000..9f648ed
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
@@ -0,0 +1,128 @@
+/*
+ * 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.launcher3.uioverrides;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+
+import android.view.MotionEvent;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.launcher3.touch.AbstractStateChangeTouchController;
+import com.android.quickstep.TouchInteractionService;
+import com.android.quickstep.util.SysuiEventLogger;
+
+/**
+ * Touch controller for handling various state transitions in portrait UI.
+ */
+public class PortraitStatesTouchController extends AbstractStateChangeTouchController {
+
+    public PortraitStatesTouchController(Launcher l) {
+        super(l, SwipeDetector.VERTICAL);
+    }
+
+    @Override
+    protected boolean canInterceptTouch(MotionEvent ev) {
+        if (mCurrentAnimation != null) {
+            // If we are already animating from a previous state, we can intercept.
+            return true;
+        }
+        if (mLauncher.isInState(ALL_APPS)) {
+            // In all-apps only listen if the container cannot scroll itself
+            if (!mLauncher.getAppsView().shouldContainerScroll(ev)) {
+                return false;
+            }
+        } else {
+            // For all other states, only listen if the event originated below the hotseat height
+            DeviceProfile dp = mLauncher.getDeviceProfile();
+            int hotseatHeight = dp.hotseatBarSizePx + dp.getInsets().bottom;
+            if (ev.getY() < (mLauncher.getDragLayer().getHeight() - hotseatHeight)) {
+                return false;
+            }
+        }
+        if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    protected int getSwipeDirection(MotionEvent ev) {
+        final int directionsToDetectScroll;
+        if (mLauncher.isInState(ALL_APPS)) {
+            directionsToDetectScroll = SwipeDetector.DIRECTION_NEGATIVE;
+            mStartContainerType = ContainerType.ALLAPPS;
+        } else if (mLauncher.isInState(NORMAL)) {
+            directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
+            mStartContainerType = ContainerType.HOTSEAT;
+        } else if (mLauncher.isInState(OVERVIEW)) {
+            directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
+            mStartContainerType = ContainerType.TASKSWITCHER;
+        } else {
+            return 0;
+        }
+        mFromState = mLauncher.getStateManager().getState();
+        mToState = getTargetState();
+        if (mFromState == mToState) {
+            return 0;
+        }
+        return directionsToDetectScroll;
+    }
+
+    protected LauncherState getTargetState() {
+        if (mLauncher.isInState(ALL_APPS)) {
+            // Should swipe down go to OVERVIEW instead?
+            return TouchInteractionService.isConnected() ?
+                    mLauncher.getStateManager().getLastState() : NORMAL;
+        } else if (mLauncher.isInState(OVERVIEW)) {
+            return ALL_APPS;
+        } else {
+            return TouchInteractionService.isConnected() ? OVERVIEW : ALL_APPS;
+        }
+    }
+
+    @Override
+    protected float initCurrentAnimation() {
+        float range = getShiftRange();
+        long maxAccuracy = (long) (2 * range);
+        mCurrentAnimation = mLauncher.getStateManager()
+                .createAnimationToNewWorkspace(mToState, maxAccuracy);
+
+        float startVerticalShift = mFromState.getVerticalProgress(mLauncher) * range;
+        float endVerticalShift = mToState.getVerticalProgress(mLauncher) * range;
+
+        float totalShift = endVerticalShift - startVerticalShift;
+        if (totalShift == 0) {
+            totalShift = Math.signum(mFromState.ordinal - mToState.ordinal)
+                    * OverviewState.getDefaultSwipeHeight(mLauncher);
+        }
+        return 1 / totalShift;
+    }
+
+    @Override
+    protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
+        super.onSwipeInteractionCompleted(targetState, logAction);
+        if (mFromState == NORMAL && targetState == OVERVIEW) {
+            SysuiEventLogger.writeDummyRecentsTransition(0);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TaggedAnimatorSetBuilder.java b/quickstep/src/com/android/launcher3/uioverrides/TaggedAnimatorSetBuilder.java
deleted file mode 100644
index 651a753..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/TaggedAnimatorSetBuilder.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2017 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.launcher3.uioverrides;
-
-import android.animation.Animator;
-import android.util.SparseArray;
-
-import com.android.launcher3.anim.AnimatorSetBuilder;
-
-import java.util.Collections;
-import java.util.List;
-
-public class TaggedAnimatorSetBuilder extends AnimatorSetBuilder {
-
-    /**
-     * Map of the index in {@link #mAnims} to tag. All the animations in {@link #mAnims} starting
-     * from this index correspond to the tag (until a new tag is specified for an index)
-     */
-    private final SparseArray<Object> mTags = new SparseArray<>();
-
-    @Override
-    public void startTag(Object obj) {
-        mTags.put(mAnims.size(), obj);
-    }
-
-    public List<Animator> getAnimationsForTag(Object tag) {
-        int startIndex = mTags.indexOfValue(tag);
-        if (startIndex < 0) {
-            return Collections.emptyList();
-        }
-        int startPos = mTags.keyAt(startIndex);
-
-        int endIndex = startIndex + 1;
-        int endPos = endIndex >= mTags.size() ? mAnims.size() : mTags.keyAt(endIndex);
-
-        return mAnims.subList(startPos, endPos);
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
similarity index 71%
rename from quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java
rename to quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
index 8b73809..d11547d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
@@ -15,8 +15,6 @@
  */
 package com.android.launcher3.uioverrides;
 
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 
@@ -30,24 +28,21 @@
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.touch.SwipeDetector;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
 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.launcher3.util.TouchController;
 import com.android.quickstep.PendingAnimation;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 
 /**
- * Touch controller for swipe interaction in Overview state
+ * Touch controller for handling task view card swipes
  */
-public class OverviewSwipeController extends AnimatorListenerAdapter
+public class TaskViewTouchController extends AnimatorListenerAdapter
         implements TouchController, SwipeDetector.Listener {
 
     private static final String TAG = "OverviewSwipeController";
@@ -68,16 +63,14 @@
     private boolean mCurrentAnimationIsGoingUp;
 
     private boolean mNoIntercept;
-    private boolean mSwipeDownEnabled;
 
     private float mDisplacementShift;
     private float mProgressMultiplier;
     private float mEndDisplacement;
-    private int mStartingTarget;
 
     private TaskView mTaskBeingDragged;
 
-    public OverviewSwipeController(Launcher launcher) {
+    public TaskViewTouchController(Launcher launcher) {
         mLauncher = launcher;
         mRecentsView = launcher.getOverviewPanel();
         mDetector = new SwipeDetector(launcher, this, SwipeDetector.VERTICAL);
@@ -94,15 +87,6 @@
         return mLauncher.isInState(OVERVIEW);
     }
 
-    private boolean isEventOverHotseat(MotionEvent ev) {
-        if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
-            return ev.getY() >
-                    mLauncher.getDragLayer().getHeight() * OVERVIEW.getVerticalProgress(mLauncher);
-        } else {
-            return mLauncher.getDragLayer().isEventOverHotseat(ev);
-        }
-    }
-
     @Override
     public void onAnimationCancel(Animator animation) {
         if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) {
@@ -129,22 +113,14 @@
                 ignoreSlopWhenSettling = true;
             } else {
                 mTaskBeingDragged = null;
-                mSwipeDownEnabled = true;
 
                 View view = mRecentsView.getChildAt(mRecentsView.getCurrentPage());
                 if (view instanceof TaskView && mLauncher.getDragLayer().isEventOverView(view, ev)) {
                     // The tile can be dragged down to open the task.
                     mTaskBeingDragged = (TaskView) view;
                     directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
-                    mStartingTarget = LauncherLogProto.ItemType.TASK;
-                } else if (isEventOverHotseat(ev)) {
-                    // The hotseat is being dragged
-                    directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
-                    mSwipeDownEnabled = false;
-                    mStartingTarget = ContainerType.HOTSEAT;
                 } else {
                     mNoIntercept = true;
-                    mStartingTarget = ContainerType.WORKSPACE;
                     return false;
                 }
             }
@@ -167,9 +143,6 @@
     }
 
     private void reInitAnimationController(boolean goingUp) {
-        if (!goingUp && !mSwipeDownEnabled) {
-            goingUp = true;
-        }
         if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) {
             // No need to init
             return;
@@ -187,31 +160,20 @@
         long maxDuration = (long) (2 * range);
         DragLayer dl = mLauncher.getDragLayer();
 
-        if (mTaskBeingDragged == null) {
-            // User is either going to all apps or home
-            mCurrentAnimation = mLauncher.getStateManager()
-                    .createAnimationToNewWorkspace(goingUp ? ALL_APPS : NORMAL, maxDuration);
-            if (goingUp) {
-                mEndDisplacement = -range;
-            } else {
-                mEndDisplacement = EdgeSwipeController.getShiftRange(mLauncher);
-            }
+        if (goingUp) {
+            mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged,
+                    true /* animateTaskView */, true /* removeTask */, maxDuration);
+            mCurrentAnimation = AnimatorPlaybackController
+                    .wrap(mPendingAnimation.anim, maxDuration);
+            mEndDisplacement = -mTaskBeingDragged.getHeight();
         } else {
-            if (goingUp) {
-                mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged,
-                        true /* animateTaskView */, true /* removeTask */, maxDuration);
-                mCurrentAnimation = AnimatorPlaybackController
-                        .wrap(mPendingAnimation.anim, maxDuration);
-                mEndDisplacement = -mTaskBeingDragged.getHeight();
-            } else {
-                AnimatorSet anim = new AnimatorSet();
-                // TODO: Setup a zoom animation
-                mCurrentAnimation = AnimatorPlaybackController.wrap(anim, maxDuration);
+            AnimatorSet anim = new AnimatorSet();
+            // TODO: Setup a zoom animation
+            mCurrentAnimation = AnimatorPlaybackController.wrap(anim, maxDuration);
 
-                mTempCords[1] = mTaskBeingDragged.getHeight();
-                dl.getDescendantCoordRelativeToSelf(mTaskBeingDragged, mTempCords);
-                mEndDisplacement = dl.getHeight() - mTempCords[1];
-            }
+            mTempCords[1] = mTaskBeingDragged.getHeight();
+            dl.getDescendantCoordRelativeToSelf(mTaskBeingDragged, mTempCords);
+            mEndDisplacement = dl.getHeight() - mTempCords[1];
         }
 
         mCurrentAnimation.getTarget().addListener(this);
@@ -249,9 +211,7 @@
         if (fling) {
             logAction = Touch.FLING;
             boolean goingUp = velocity < 0;
-            if (!goingUp && !mSwipeDownEnabled) {
-                goingToEnd = false;
-            } else if (goingUp != mCurrentAnimationIsGoingUp) {
+            if (goingUp != mCurrentAnimationIsGoingUp) {
                 // In case the fling is in opposite direction, make sure if is close enough
                 // from the start position
                 if (mCurrentAnimation.getProgressFraction()
@@ -277,7 +237,6 @@
         float nextFrameProgress = Utilities.boundToRange(
                 progress + velocity * SINGLE_FRAME_MS / Math.abs(mEndDisplacement), 0f, 1f);
 
-
         mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd, logAction));
 
         ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
@@ -292,25 +251,13 @@
             mPendingAnimation.finish(wasSuccess);
             mPendingAnimation = null;
         }
-        if (mTaskBeingDragged == null) {
-            LauncherState state = wasSuccess ?
-                    (mCurrentAnimationIsGoingUp ? ALL_APPS : NORMAL) : OVERVIEW;
-            mLauncher.getStateManager().goToState(state, false);
-
-        } else if (wasSuccess) {
+        if (wasSuccess) {
             if (!mCurrentAnimationIsGoingUp) {
                 mTaskBeingDragged.launchTask(false);
                 mLauncher.getUserEventDispatcher().logTaskLaunch(logAction,
                         Direction.DOWN, mTaskBeingDragged.getTask().getTopComponent());
             }
         }
-        if (mTaskBeingDragged == null || (wasSuccess && mCurrentAnimationIsGoingUp)) {
-            mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
-                    mCurrentAnimationIsGoingUp ? Direction.UP : Direction.DOWN,
-                    mStartingTarget, ContainerType.TASKSWITCHER,
-                    mLauncher.getStateManager().getState().containerType,
-                    mRecentsView.getCurrentPage());
-        }
         mDetector.finishedScrolling();
         mTaskBeingDragged = null;
         mCurrentAnimation = null;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java
deleted file mode 100644
index c8d75dc..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java
+++ /dev/null
@@ -1,447 +0,0 @@
-/*
- * Copyright (C) 2017 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.launcher3.uioverrides;
-
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
-import static com.android.quickstep.TouchInteractionService.EDGE_NAV_BAR;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.util.Log;
-import android.view.MotionEvent;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
-import com.android.launcher3.LauncherStateManager.AnimationConfig;
-import com.android.launcher3.LauncherStateManager.StateHandler;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.anim.AnimatorSetBuilder;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.touch.SwipeDetector;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.util.FloatRange;
-import com.android.launcher3.util.TouchController;
-import com.android.quickstep.TouchInteractionService;
-
-/**
- * Handles vertical touch gesture on the DragLayer
- */
-public class TwoStepSwipeController extends AnimatorListenerAdapter
-        implements TouchController, SwipeDetector.Listener {
-
-    private static final String TAG = "TwoStepSwipeController";
-
-    private static final float RECATCH_REJECTION_FRACTION = .0875f;
-    private static final int SINGLE_FRAME_MS = 16;
-    private static final long QUICK_SNAP_TO_OVERVIEW_DURATION = 250;
-
-    // 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;
-
-    /**
-     * Index of the vertical swipe handles in {@link LauncherStateManager#getStateHandlers()}.
-     */
-    private static final int SWIPE_HANDLER_INDEX = 0;
-
-    /**
-     * Index of various UI handlers in {@link LauncherStateManager#getStateHandlers()} not related
-     * to vertical swipe.
-     */
-    private static final int OTHER_HANDLERS_START_INDEX = SWIPE_HANDLER_INDEX + 1;
-
-    // Swipe progress range (when starting from NORMAL state) where OVERVIEW state is allowed
-    private static final float MIN_PROGRESS_TO_OVERVIEW = 0.1f;
-    private static final float MAX_PROGRESS_TO_OVERVIEW = 0.4f;
-
-    private static final int FLAG_OVERVIEW_DISABLED_OUT_OF_RANGE = 1 << 0;
-    private static final int FLAG_OVERVIEW_DISABLED_FLING = 1 << 1;
-    private static final int FLAG_OVERVIEW_DISABLED_CANCEL_STATE = 1 << 2;
-    private static final int FLAG_OVERVIEW_DISABLED = 1 << 4;
-    private static final int FLAG_DISABLED_TWO_TARGETS = 1 << 5;
-    private static final int FLAG_DISABLED_BACK_TARGET = 1 << 6;
-
-    private final Launcher mLauncher;
-    private final SwipeDetector mDetector;
-
-    private boolean mNoIntercept;
-    private int mStartContainerType;
-
-    private DragPauseDetector mDragPauseDetector;
-    private FloatRange mOverviewProgressRange;
-    private TaggedAnimatorSetBuilder mTaggedAnimatorSetBuilder;
-    private AnimatorSet mQuickOverviewAnimation;
-    private boolean mAnimatingToOverview;
-    private CroppedAnimationController mCroppedAnimationController;
-
-    private AnimatorPlaybackController mCurrentAnimation;
-    private LauncherState mFromState;
-    private LauncherState mToState;
-
-    private float mStartProgress;
-    // Ratio of transition process [0, 1] to drag displacement (px)
-    private float mProgressMultiplier;
-
-    public TwoStepSwipeController(Launcher l) {
-        mLauncher = l;
-        mDetector = new SwipeDetector(l, this, SwipeDetector.VERTICAL);
-    }
-
-    private boolean canInterceptTouch(MotionEvent ev) {
-        if (mCurrentAnimation != null) {
-            // If we are already animating from a previous state, we can intercept.
-            return true;
-        }
-        if (mLauncher.isInState(NORMAL)) {
-            if ((ev.getEdgeFlags() & EDGE_NAV_BAR) != 0 &&
-                    !mLauncher.getDeviceProfile().isVerticalBarLayout()) {
-                // On normal swipes ignore edge swipes
-                return false;
-            }
-        } else if (mLauncher.isInState(ALL_APPS)) {
-            if (!mLauncher.getAppsView().shouldContainerScroll(ev)) {
-                return false;
-            }
-        } else {
-            // Don't listen for the swipe gesture if we are already in some other state.
-            return false;
-        }
-        if (mAnimatingToOverview) {
-            return false;
-        }
-        if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
-            return false;
-        }
-
-        return true;
-    }
-
-    @Override
-    public void onAnimationCancel(Animator animation) {
-        if (mCurrentAnimation != null && animation == mCurrentAnimation.getOriginalTarget()) {
-            Log.e(TAG, "Who dare cancel the animation when I am in control", new Exception());
-            clearState();
-        }
-    }
-
-    @Override
-    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            mNoIntercept = !canInterceptTouch(ev);
-            if (mNoIntercept) {
-                return false;
-            }
-
-            // Now figure out which direction scroll events the controller will start
-            // calling the callbacks.
-            final int directionsToDetectScroll;
-            boolean ignoreSlopWhenSettling = false;
-
-            if (mCurrentAnimation != null) {
-                if (mCurrentAnimation.getProgressFraction() > 1 - RECATCH_REJECTION_FRACTION) {
-                    directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
-                } else if (mCurrentAnimation.getProgressFraction() < RECATCH_REJECTION_FRACTION ) {
-                    directionsToDetectScroll = SwipeDetector.DIRECTION_NEGATIVE;
-                } else {
-                    directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
-                    ignoreSlopWhenSettling = true;
-                }
-            } else {
-                if (mLauncher.isInState(ALL_APPS)) {
-                    directionsToDetectScroll = SwipeDetector.DIRECTION_NEGATIVE;
-                    mStartContainerType = ContainerType.ALLAPPS;
-                } else {
-                    directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
-                    mStartContainerType = mLauncher.getDragLayer().isEventOverHotseat(ev) ?
-                            ContainerType.HOTSEAT : ContainerType.WORKSPACE;
-                }
-            }
-
-            mDetector.setDetectableScrollConditions(
-                    directionsToDetectScroll, ignoreSlopWhenSettling);
-        }
-
-        if (mNoIntercept) {
-            return false;
-        }
-
-        onControllerTouchEvent(ev);
-        return mDetector.isDraggingOrSettling();
-    }
-
-    @Override
-    public boolean onControllerTouchEvent(MotionEvent ev) {
-        return mDetector.onTouchEvent(ev);
-    }
-
-    @Override
-    public void onDragStart(boolean start) {
-        if (mCurrentAnimation == null) {
-            float range = getShiftRange();
-            long maxAccuracy = (long) (2 * range);
-
-            mDragPauseDetector = new DragPauseDetector(this::onDragPauseDetected);
-            mDragPauseDetector.addDisabledFlags(FLAG_OVERVIEW_DISABLED_OUT_OF_RANGE);
-            if (FeatureFlags.ENABLE_TWO_SWIPE_TARGETS) {
-                mDragPauseDetector.addDisabledFlags(FLAG_DISABLED_TWO_TARGETS);
-            }
-
-            mOverviewProgressRange = new FloatRange();
-            mOverviewProgressRange.start = mLauncher.isInState(NORMAL)
-                    ? MIN_PROGRESS_TO_OVERVIEW
-                    : 1 - MAX_PROGRESS_TO_OVERVIEW;
-            mOverviewProgressRange.end = mOverviewProgressRange.start
-                    + MAX_PROGRESS_TO_OVERVIEW - MIN_PROGRESS_TO_OVERVIEW;
-
-            // Build current animation
-            mFromState = mLauncher.getStateManager().getState();
-            mToState = mLauncher.isInState(ALL_APPS) ? NORMAL : ALL_APPS;
-
-            if (mToState == NORMAL && mLauncher.getStateManager().getLastState() == OVERVIEW) {
-                mToState = OVERVIEW;
-                mDragPauseDetector.addDisabledFlags(FLAG_DISABLED_BACK_TARGET);
-            }
-
-            mTaggedAnimatorSetBuilder = new TaggedAnimatorSetBuilder();
-            mCurrentAnimation = mLauncher.getStateManager().createAnimationToNewWorkspace(
-                    mToState, mTaggedAnimatorSetBuilder, maxAccuracy);
-
-            if (!TouchInteractionService.isConnected()) {
-                mDragPauseDetector.addDisabledFlags(FLAG_OVERVIEW_DISABLED);
-            }
-
-            mCurrentAnimation.getTarget().addListener(this);
-            mStartProgress = 0;
-            mProgressMultiplier = (mLauncher.isInState(ALL_APPS) ? 1 : -1) / range;
-            mCurrentAnimation.dispatchOnStart();
-        } else {
-            mCurrentAnimation.pause();
-            mStartProgress = mCurrentAnimation.getProgressFraction();
-
-            mDragPauseDetector.clearDisabledFlags(FLAG_OVERVIEW_DISABLED_FLING);
-            updatePauseDetectorRangeFlag();
-        }
-    }
-
-    private float getShiftRange() {
-        return mLauncher.getAllAppsController().getShiftRange();
-    }
-
-    @Override
-    public boolean onDrag(float displacement, float velocity) {
-        float deltaProgress = mProgressMultiplier * displacement;
-        mCurrentAnimation.setPlayFraction(deltaProgress + mStartProgress);
-
-        updatePauseDetectorRangeFlag();
-        mDragPauseDetector.onDrag(velocity);
-
-        return true;
-    }
-
-    private void updatePauseDetectorRangeFlag() {
-        if (mOverviewProgressRange.contains(mCurrentAnimation.getProgressFraction())) {
-            mDragPauseDetector.clearDisabledFlags(FLAG_OVERVIEW_DISABLED_OUT_OF_RANGE);
-        } else {
-            mDragPauseDetector.addDisabledFlags(FLAG_OVERVIEW_DISABLED_OUT_OF_RANGE);
-        }
-    }
-
-    @Override
-    public void onDragEnd(float velocity, boolean fling) {
-        mDragPauseDetector.addDisabledFlags(FLAG_OVERVIEW_DISABLED_FLING);
-
-        final int logAction;
-        LauncherState targetState;
-        final float progress = mCurrentAnimation.getProgressFraction();
-
-        if (fling) {
-            logAction = Touch.FLING;
-            targetState = velocity < 0 ? ALL_APPS : mLauncher.getStateManager().getLastState();
-            // snap to top or bottom using the release velocity
-        } else {
-            logAction = Touch.SWIPE;
-            targetState = (progress > SUCCESS_TRANSITION_PROGRESS) ? mToState : mFromState;
-        }
-
-        float endProgress;
-
-        if (mDragPauseDetector.isTriggered() && targetState == NORMAL) {
-            targetState = OVERVIEW;
-            endProgress = OVERVIEW.getVerticalProgress(mLauncher);
-            if (mFromState == NORMAL) {
-                endProgress = 1 - endProgress;
-            }
-        } else if (targetState == mToState) {
-            endProgress = 1;
-        } else {
-            endProgress = 0;
-        }
-
-        LauncherState targetStateFinal = targetState;
-        mCurrentAnimation.setEndAction(() ->
-                onSwipeInteractionCompleted(targetStateFinal, logAction));
-
-        float nextFrameProgress = Utilities.boundToRange(
-                progress + velocity * SINGLE_FRAME_MS / getShiftRange(), 0f, 1f);
-
-        ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
-        anim.setFloatValues(nextFrameProgress, endProgress);
-        anim.setDuration(
-                SwipeDetector.calculateDuration(velocity, Math.abs(endProgress - progress)));
-        anim.setInterpolator(scrollInterpolatorForVelocity(velocity));
-        anim.start();
-    }
-
-    private void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
-        if (targetState != mFromState) {
-            // Transition complete. log the action
-            mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
-                    mToState == ALL_APPS ? Direction.UP : Direction.DOWN,
-                    mStartContainerType,
-                    mFromState.containerType,
-                    mToState.containerType,
-                    mLauncher.getWorkspace().getCurrentPage());
-        }
-        clearState();
-
-        // TODO: mQuickOverviewAnimation might still be running in which changing a state instantly
-        // may cause a jump. Animate the state change with a short duration in this case?
-        mLauncher.getStateManager().goToState(targetState, false /* animated */);
-    }
-
-    private void onDragPauseDetected() {
-        final ValueAnimator twoStepAnimator = ValueAnimator.ofFloat(0, 1);
-        twoStepAnimator.setDuration(mCurrentAnimation.getDuration());
-        StateHandler[] handlers = mLauncher.getStateManager().getStateHandlers();
-
-        // Change the current animation to only play the vertical handle
-        AnimatorSet anim = new AnimatorSet();
-        anim.playTogether(mTaggedAnimatorSetBuilder.getAnimationsForTag(
-                handlers[SWIPE_HANDLER_INDEX]));
-        anim.play(twoStepAnimator);
-        mCurrentAnimation = mCurrentAnimation.cloneFor(anim);
-
-        AnimatorSetBuilder builder = new AnimatorSetBuilder();
-        AnimationConfig config = new AnimationConfig();
-        config.duration = QUICK_SNAP_TO_OVERVIEW_DURATION;
-        for (int i = OTHER_HANDLERS_START_INDEX; i < handlers.length; i++) {
-            handlers[i].setStateWithAnimation(OVERVIEW, builder, config);
-        }
-        mQuickOverviewAnimation = builder.build();
-        mQuickOverviewAnimation.addListener(new AnimationSuccessListener() {
-            @Override
-            public void onAnimationSuccess(Animator animator) {
-                onQuickOverviewAnimationComplete(twoStepAnimator);
-            }
-        });
-        mQuickOverviewAnimation.start();
-    }
-
-    private void onQuickOverviewAnimationComplete(ValueAnimator animator) {
-        if (mAnimatingToOverview) {
-            return;
-        }
-
-        // For the remainder to the interaction, the user can either go to the ALL_APPS state or
-        // the OVERVIEW state.
-        // The remaining state handlers are on the OVERVIEW state. Create one animation towards the
-        // ALL_APPS state and only call it when the user moved above the current range.
-        AnimationConfig config = new AnimationConfig();
-        config.duration = (long) (2 * getShiftRange());
-        config.userControlled = true;
-
-        AnimatorSetBuilder builderToAllAppsState = new AnimatorSetBuilder();
-        StateHandler[] handlers = mLauncher.getStateManager().getStateHandlers();
-        for (int i = OTHER_HANDLERS_START_INDEX; i < handlers.length; i++) {
-            handlers[i].setStateWithAnimation(ALL_APPS, builderToAllAppsState, config);
-        }
-
-        mCroppedAnimationController = new CroppedAnimationController(
-                AnimatorPlaybackController.wrap(builderToAllAppsState.build(), config.duration),
-                new FloatRange(animator.getAnimatedFraction(), mToState == ALL_APPS ? 1 : 0));
-        animator.addUpdateListener(mCroppedAnimationController);
-    }
-
-    private void clearState() {
-        mCurrentAnimation = null;
-        mTaggedAnimatorSetBuilder = null;
-        if (mDragPauseDetector != null) {
-            mDragPauseDetector.addDisabledFlags(FLAG_OVERVIEW_DISABLED_CANCEL_STATE);
-        }
-        mDragPauseDetector = null;
-
-        if (mQuickOverviewAnimation != null) {
-            mQuickOverviewAnimation.cancel();
-            mQuickOverviewAnimation = null;
-        }
-        mCroppedAnimationController = null;
-        mAnimatingToOverview = false;
-
-        mDetector.finishedScrolling();
-    }
-
-    /**
-     * {@link AnimatorUpdateListener} which controls another animation for a fraction of range
-     */
-    private static class CroppedAnimationController implements AnimatorUpdateListener {
-
-        private final AnimatorPlaybackController mTarget;
-        private final FloatRange mRange;
-
-        CroppedAnimationController(AnimatorPlaybackController target, FloatRange range) {
-            mTarget = target;
-            mRange = range;
-        }
-
-
-        @Override
-        public void onAnimationUpdate(ValueAnimator valueAnimator) {
-            float fraction = valueAnimator.getAnimatedFraction();
-
-            if (mRange.start < mRange.end) {
-                if (fraction <= mRange.start) {
-                    mTarget.setPlayFraction(0);
-                } else if (fraction >= mRange.end) {
-                    mTarget.setPlayFraction(1);
-                } else {
-                    mTarget.setPlayFraction((fraction - mRange.start) / (mRange.end - mRange.start));
-                }
-            } else if (mRange.start > mRange.end) {
-                if (fraction >= mRange.start) {
-                    mTarget.setPlayFraction(0);
-                } else if (fraction <= mRange.end) {
-                    mTarget.setPlayFraction(1);
-                } else {
-                    mTarget.setPlayFraction((fraction - mRange.start) / (mRange.end - mRange.start));
-                }
-            } else {
-                // mRange.start == mRange.end
-                mTarget.setPlayFraction(0);
-            }
-        }
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
index 789185a..49792ac 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -18,13 +18,13 @@
 
 import static com.android.launcher3.LauncherState.NORMAL;
 
+import android.content.Context;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherStateManager.StateHandler;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.util.TouchController;
 import com.android.quickstep.OverviewInteractionState;
@@ -34,17 +34,17 @@
 public class UiFactory {
 
     public static TouchController[] createTouchControllers(Launcher launcher) {
-        if (FeatureFlags.ENABLE_TWO_SWIPE_TARGETS) {
+        if (launcher.getDeviceProfile().isVerticalBarLayout()) {
             return new TouchController[] {
                     launcher.getDragController(),
-                    new EdgeSwipeController(launcher),
-                    new TwoStepSwipeController(launcher),
-                    new OverviewSwipeController(launcher)};
+                    new LandscapeStatesTouchController(launcher),
+                    new LandscapeEdgeSwipeController(launcher),
+                    new TaskViewTouchController(launcher)};
         } else {
             return new TouchController[] {
                     launcher.getDragController(),
-                    new TwoStepSwipeController(launcher),
-                    new OverviewSwipeController(launcher)};
+                    new PortraitStatesTouchController(launcher),
+                    new TaskViewTouchController(launcher)};
         }
     }
 
@@ -73,7 +73,8 @@
                 }
             }
         }
-        OverviewInteractionState.setBackButtonVisible(launcher, shouldBackButtonBeVisible);
+        OverviewInteractionState.getInstance(launcher)
+                .setBackButtonVisible(shouldBackButtonBeVisible);
     }
 
     public static void resetOverview(Launcher launcher) {
@@ -81,15 +82,15 @@
         recents.reset();
     }
 
-    public static void onStart(Launcher launcher) {
-        RecentsModel model = RecentsModel.getInstance(launcher);
+    public static void onStart(Context context) {
+        RecentsModel model = RecentsModel.getInstance(context);
         if (model != null) {
             model.onStart();
         }
     }
 
-    public static void onTrimMemory(Launcher launcher, int level) {
-        RecentsModel model = RecentsModel.getInstance(launcher);
+    public static void onTrimMemory(Context context, int level) {
+        RecentsModel model = RecentsModel.getInstance(context);
         if (model != null) {
             model.onTrimMemory(level);
         }
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
new file mode 100644
index 0000000..b43352a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -0,0 +1,312 @@
+/*
+ * 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.FAST_OVERVIEW;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.View;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherInitListener;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.allapps.AllAppsTransitionController;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.util.ViewOnDrawExecutor;
+import com.android.quickstep.views.LauncherLayoutListener;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
+import com.android.systemui.shared.system.AssistDataReceiver;
+import com.android.systemui.shared.system.RecentsAnimationListener;
+import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
+
+import java.util.function.BiPredicate;
+
+/**
+ * Utility class which abstracts out the logical differences between Launcher and RecentsActivity.
+ */
+public interface ActivityControlHelper<T extends BaseDraggingActivity> {
+
+    LayoutListener createLayoutListener(T activity);
+
+    void onQuickstepGestureStarted(T activity, boolean activityVisible);
+
+    void onQuickInteractionStart(T activity, boolean activityVisible);
+
+    void executeOnNextDraw(T activity, TaskView targetView, Runnable action);
+
+    void onTransitionCancelled(T activity, boolean activityVisible);
+
+    int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect);
+
+    void onSwipeUpComplete(T activity);
+
+    void prepareRecentsUI(T activity, boolean activityVisible);
+
+    AnimatorPlaybackController createControllerForVisibleActivity(T activity);
+
+    AnimatorPlaybackController createControllerForHiddenActivity(T activity, int transitionLength);
+
+    ActivityInitListener createActivityInitListener(BiPredicate<T, Boolean> onInitListener);
+
+    void startRecents(Context context, Intent intent, AssistDataReceiver assistDataReceiver,
+            RecentsAnimationListener remoteAnimationListener);
+
+    class LauncherActivityControllerHelper implements ActivityControlHelper<Launcher> {
+
+        @Override
+        public LayoutListener createLayoutListener(Launcher activity) {
+            return new LauncherLayoutListener(activity);
+        }
+
+        @Override
+        public void onQuickstepGestureStarted(Launcher activity, boolean activityVisible) {
+            activity.onQuickstepGestureStarted(activityVisible);
+        }
+
+        @Override
+        public void onQuickInteractionStart(Launcher activity, boolean activityVisible) {
+            activity.getStateManager().goToState(FAST_OVERVIEW, activityVisible);
+        }
+
+        @Override
+        public void executeOnNextDraw(Launcher activity, TaskView targetView, Runnable action) {
+            ViewOnDrawExecutor executor = new ViewOnDrawExecutor() {
+                @Override
+                public void onViewDetachedFromWindow(View v) {
+                    if (!isCompleted()) {
+                        runAllTasks();
+                    }
+                }
+            };
+            executor.attachTo(activity, targetView, false /* waitForLoadAnimation */);
+            executor.execute(action);
+        }
+
+        @Override
+        public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
+            RecentsView.getPageRect(dp, context, outRect);
+            if (dp.isVerticalBarLayout()) {
+                Rect targetInsets = dp.getInsets();
+                int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
+                return dp.hotseatBarSizePx + dp.hotseatBarSidePaddingPx + hotseatInset;
+            } else {
+                return dp.heightPx - outRect.bottom;
+            }
+        }
+
+        @Override
+        public void onTransitionCancelled(Launcher activity, boolean activityVisible) {
+            LauncherState startState = activity.getStateManager().getRestState();
+            activity.getStateManager().goToState(startState, activityVisible);
+        }
+
+        @Override
+        public void onSwipeUpComplete(Launcher activity) {
+            // Re apply state in case we did something funky during the transition.
+            activity.getStateManager().reapplyState();
+        }
+
+        @Override
+        public void prepareRecentsUI(Launcher activity, boolean activityVisible) {
+            LauncherState startState = activity.getStateManager().getState();
+            if (startState.disableRestore) {
+                startState = activity.getStateManager().getRestState();
+            }
+            activity.getStateManager().setRestState(startState);
+
+            if (!activityVisible) {
+                // Since the launcher is not visible, we can safely reset the scroll position.
+                // This ensures then the next swipe up to all-apps starts from scroll 0.
+                activity.getAppsView().reset(false /* animate */);
+                activity.getStateManager().goToState(OVERVIEW, false);
+
+                // Optimization, hide the all apps view to prevent layout while initializing
+                activity.getAppsView().getContentView().setVisibility(View.GONE);
+            }
+        }
+
+        @Override
+        public AnimatorPlaybackController createControllerForVisibleActivity(Launcher activity) {
+            DeviceProfile dp = activity.getDeviceProfile();
+            long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx);
+            return activity.getStateManager().createAnimationToNewWorkspace(OVERVIEW, accuracy);
+        }
+
+        @Override
+        public AnimatorPlaybackController createControllerForHiddenActivity(
+                Launcher activity, int transitionLength) {
+            AllAppsTransitionController controller = activity.getAllAppsController();
+            AnimatorSet anim = new AnimatorSet();
+            if (activity.getDeviceProfile().isVerticalBarLayout()) {
+                // TODO:
+            } else {
+                float scrollRange = Math.max(controller.getShiftRange(), 1);
+                float progressDelta = (transitionLength / scrollRange);
+
+                float endProgress = OVERVIEW.getVerticalProgress(activity);
+                float startProgress = endProgress + progressDelta;
+                ObjectAnimator shiftAnim = ObjectAnimator.ofFloat(
+                        controller, ALL_APPS_PROGRESS, startProgress, endProgress);
+                shiftAnim.setInterpolator(LINEAR);
+                anim.play(shiftAnim);
+            }
+
+            // TODO: Link this animation to state animation, so that it is cancelled
+            // automatically on state change
+            anim.setDuration(transitionLength * 2);
+            return AnimatorPlaybackController.wrap(anim, transitionLength * 2);
+        }
+
+        @Override
+        public ActivityInitListener createActivityInitListener(
+                BiPredicate<Launcher, Boolean> onInitListener) {
+            return new LauncherInitListener(onInitListener);
+        }
+
+        @Override
+        public void startRecents(Context context, Intent intent,
+                AssistDataReceiver assistDataReceiver,
+                RecentsAnimationListener remoteAnimationListener) {
+            ActivityManagerWrapper.getInstance().startRecentsActivity(
+                    intent, assistDataReceiver, remoteAnimationListener, null, null);
+        }
+    }
+
+    class FallbackActivityControllerHelper implements ActivityControlHelper<RecentsActivity> {
+
+        @Override
+        public void onQuickstepGestureStarted(RecentsActivity activity, boolean activityVisible) {
+            // TODO:
+        }
+
+        @Override
+        public void onQuickInteractionStart(RecentsActivity activity, boolean activityVisible) {
+            // TODO:
+        }
+
+        @Override
+        public void executeOnNextDraw(RecentsActivity activity, TaskView targetView,
+                Runnable action) {
+            // TODO:
+            new Handler(Looper.getMainLooper()).post(action);
+        }
+
+        @Override
+        public void onTransitionCancelled(RecentsActivity activity, boolean activityVisible) {
+            // TODO:
+        }
+
+        @Override
+        public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
+            FallbackRecentsView.getCenterPageRect(dp, context, outRect);
+            if (dp.isVerticalBarLayout()) {
+                Rect targetInsets = dp.getInsets();
+                int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
+                return dp.hotseatBarSizePx + dp.hotseatBarSidePaddingPx + hotseatInset;
+            } else {
+                return dp.heightPx - outRect.bottom;
+            }
+        }
+
+        @Override
+        public void onSwipeUpComplete(RecentsActivity activity) {
+            // TODO:
+        }
+
+        @Override
+        public void prepareRecentsUI(RecentsActivity activity, boolean activityVisible) {
+            // TODO:
+        }
+
+        @Override
+        public AnimatorPlaybackController createControllerForVisibleActivity(
+                RecentsActivity activity) {
+            DeviceProfile dp = activity.getDeviceProfile();
+            return createControllerForHiddenActivity(activity, Math.max(dp.widthPx, dp.heightPx));
+        }
+
+        @Override
+        public AnimatorPlaybackController createControllerForHiddenActivity(
+                RecentsActivity activity, int transitionLength) {
+            // We do not animate anything. Create a empty controller
+            AnimatorSet anim = new AnimatorSet();
+            return AnimatorPlaybackController.wrap(anim, transitionLength * 2);
+        }
+
+        @Override
+        public LayoutListener createLayoutListener(RecentsActivity activity) {
+            // We do not change anything as part of layout changes in fallback activity. Return a
+            // default layout listener.
+            return new LayoutListener() {
+                @Override
+                public void open() { }
+
+                @Override
+                public void setHandler(WindowTransformSwipeHandler handler) { }
+
+                @Override
+                public void finish() { }
+            };
+        }
+
+        @Override
+        public ActivityInitListener createActivityInitListener(
+                BiPredicate<RecentsActivity, Boolean> onInitListener) {
+            return new RecentsActivityTracker(onInitListener);
+        }
+
+        @Override
+        public void startRecents(Context context, Intent intent,
+                AssistDataReceiver assistDataReceiver,
+                final RecentsAnimationListener remoteAnimationListener) {
+            ActivityOptions options =
+                    ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat(
+                            new FallbackActivityOptions(remoteAnimationListener), 10000, 10000));
+            context.startActivity(intent, options.toBundle());
+        }
+    }
+
+    interface LayoutListener {
+
+        void open();
+
+        void setHandler(WindowTransformSwipeHandler handler);
+
+        void finish();
+    }
+
+    interface ActivityInitListener {
+
+        void register();
+
+        void unregister();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/FallbackActivityOptions.java b/quickstep/src/com/android/quickstep/FallbackActivityOptions.java
new file mode 100644
index 0000000..3a7fb2d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/FallbackActivityOptions.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import android.graphics.Rect;
+import android.util.Log;
+
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
+import com.android.systemui.shared.system.RecentsAnimationListener;
+import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.WindowManagerWrapper;
+
+/**
+ * Temporary class to create activity options to emulate recents transition for fallback activtiy.
+ */
+public class FallbackActivityOptions implements RemoteAnimationRunnerCompat {
+
+    private final RecentsAnimationListener mListener;
+
+    public FallbackActivityOptions(RecentsAnimationListener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public void onAnimationStart(RemoteAnimationTargetCompat[] targetCompats,
+            Runnable runnable) {
+        DummyRecentsAnimationControllerCompat dummyRecentsAnim =
+                new DummyRecentsAnimationControllerCompat(runnable);
+
+        Rect insets = new Rect();
+        WindowManagerWrapper.getInstance().getStableInsets(insets);
+        mListener.onAnimationStart(dummyRecentsAnim, targetCompats, insets, null);
+    }
+
+    @Override
+    public void onAnimationCancelled() {
+        mListener.onAnimationCanceled();
+    }
+
+    private static class DummyRecentsAnimationControllerCompat
+            extends RecentsAnimationControllerCompat {
+
+        final Runnable mFinishCallback;
+
+        public DummyRecentsAnimationControllerCompat(Runnable finishCallback) {
+            mFinishCallback = finishCallback;
+        }
+
+        @Override
+        public ThumbnailData screenshotTask(int taskId) {
+            return new ThumbnailData();
+        }
+
+        @Override
+        public void setInputConsumerEnabled(boolean enabled) { }
+
+        @Override
+        public void setAnimationTargetsBehindSystemBars(boolean behindSystemBars) { }
+
+        @Override
+        public void finish(boolean toHome) {
+            if (toHome) {
+                mFinishCallback.run();
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/FallbackRecentsView.java
index 22f6e0c..032d753 100644
--- a/quickstep/src/com/android/quickstep/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/FallbackRecentsView.java
@@ -48,10 +48,21 @@
         setPadding(padding.left, padding.top, padding.right, padding.bottom);
     }
 
-    public static void verticalCenter(Rect padding, DeviceProfile dp) {
+    private static void verticalCenter(Rect padding, DeviceProfile dp) {
         Rect insets = dp.getInsets();
         int totalSpace = (padding.top + padding.bottom - insets.top - insets.bottom) / 2;
         padding.top = insets.top + totalSpace;
         padding.bottom = insets.bottom + totalSpace;
     }
+
+    public static void getCenterPageRect(DeviceProfile grid, Context context, Rect outRect) {
+        Rect targetPadding = getPadding(grid, context);
+        verticalCenter(targetPadding, grid);
+        Rect insets = grid.getInsets();
+        outRect.set(
+                targetPadding.left + insets.left,
+                targetPadding.top + insets.top,
+                grid.widthPx - targetPadding.right - insets.right,
+                grid.heightPx - targetPadding.bottom - insets.bottom);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
index 69e7933..ab19c6e 100644
--- a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
+++ b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
@@ -22,7 +22,6 @@
 import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.INVALID_POINTER_ID;
 
-import static com.android.quickstep.TouchInteractionService.DEBUG_SHOW_OVERVIEW_BUTTON;
 import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_BACK;
 import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_OVERVIEW;
 
@@ -36,6 +35,7 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Looper;
+import android.util.Log;
 import android.view.Choreographer;
 import android.view.Display;
 import android.view.MotionEvent;
@@ -46,7 +46,6 @@
 
 import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.util.TraceHelper;
-import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.AssistDataReceiver;
 import com.android.systemui.shared.system.BackgroundExecutor;
@@ -67,13 +66,13 @@
 public class OtherActivityTouchConsumer extends ContextWrapper implements TouchConsumer {
 
     private static final long LAUNCHER_DRAW_TIMEOUT_MS = 150;
-    private static final int[] DEFERRED_HIT_TARGETS = DEBUG_SHOW_OVERVIEW_BUTTON
+    private static final int[] DEFERRED_HIT_TARGETS = false
             ? new int[] {HIT_TARGET_BACK, HIT_TARGET_OVERVIEW} : new int[] {HIT_TARGET_BACK};
 
     private final RunningTaskInfo mRunningTask;
     private final RecentsModel mRecentsModel;
     private final Intent mHomeIntent;
-    private final ISystemUiProxy mISystemUiProxy;
+    private final ActivityControlHelper mActivityControlHelper;
     private final MainThreadExecutor mMainThreadExecutor;
     private final Choreographer mBackgroundThreadChoreographer;
 
@@ -93,7 +92,7 @@
     private boolean mIsGoingToHome;
 
     public OtherActivityTouchConsumer(Context base, RunningTaskInfo runningTaskInfo,
-            RecentsModel recentsModel, Intent homeIntent, ISystemUiProxy systemUiProxy,
+            RecentsModel recentsModel, Intent homeIntent, ActivityControlHelper activityControl,
             MainThreadExecutor mainThreadExecutor, Choreographer backgroundThreadChoreographer,
             @HitTarget int downHitTarget, VelocityTracker velocityTracker) {
         super(base);
@@ -101,7 +100,7 @@
         mRecentsModel = recentsModel;
         mHomeIntent = homeIntent;
         mVelocityTracker = velocityTracker;
-        mISystemUiProxy = systemUiProxy;
+        mActivityControlHelper = activityControl;
         mMainThreadExecutor = mainThreadExecutor;
         mBackgroundThreadChoreographer = backgroundThreadChoreographer;
         mIsDeferredDownTarget = Arrays.binarySearch(DEFERRED_HIT_TARGETS, downHitTarget) >= 0;
@@ -205,8 +204,8 @@
 
     private void startTouchTrackingForWindowAnimation(long touchTimeMs) {
         // Create the shared handler
-        final WindowTransformSwipeHandler handler =
-                new WindowTransformSwipeHandler(mRunningTask, this, touchTimeMs);
+        final WindowTransformSwipeHandler handler = new WindowTransformSwipeHandler(
+                mRunningTask, this, touchTimeMs, mActivityControlHelper);
 
         // Preload the plan
         mRecentsModel.loadTasks(mRunningTask.id, null);
@@ -223,8 +222,7 @@
         handler.initWhenReady();
 
         TraceHelper.beginSection("RecentsController");
-        Runnable startActivity = () -> ActivityManagerWrapper.getInstance()
-                .startRecentsActivity(mHomeIntent,
+        Runnable startActivity = () -> mActivityControlHelper.startRecents(this, mHomeIntent,
                 new AssistDataReceiver() {
                     @Override
                     public void onHandleAssistData(Bundle bundle) {
@@ -253,7 +251,7 @@
                             handler.onRecentsAnimationCanceled();
                         }
                     }
-                }, null, null);
+                });
 
         if (Looper.myLooper() != Looper.getMainLooper()) {
             startActivity.run();
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 5f09f00..f60473f 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -16,7 +16,6 @@
 package com.android.quickstep;
 
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.quickstep.TouchInteractionService.DEBUG_SHOW_OVERVIEW_BUTTON;
 
 import android.annotation.TargetApi;
 import android.app.ActivityManager.RecentTaskInfo;
@@ -33,6 +32,8 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.states.InternalStateHandler;
+import com.android.quickstep.ActivityControlHelper.FallbackActivityControllerHelper;
+import com.android.quickstep.ActivityControlHelper.LauncherActivityControllerHelper;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 
@@ -42,7 +43,7 @@
 @TargetApi(Build.VERSION_CODES.P)
 public class OverviewCommandHelper extends InternalStateHandler {
 
-    private static final boolean DEBUG_START_FALLBACK_ACTIVITY = DEBUG_SHOW_OVERVIEW_BUTTON;
+    private static final boolean DEBUG_START_FALLBACK_ACTIVITY = false;
 
     private final Context mContext;
     private final ActivityManagerWrapper mAM;
@@ -61,7 +62,15 @@
                 .setPackage(context.getPackageName())
                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         ResolveInfo info = context.getPackageManager().resolveActivity(homeIntent, 0);
-        launcher = new ComponentName(context.getPackageName(), info.activityInfo.name);
+
+        if (DEBUG_START_FALLBACK_ACTIVITY) {
+            launcher = new ComponentName(context, RecentsActivity.class);
+            homeIntent.addCategory(Intent.CATEGORY_DEFAULT)
+                    .removeCategory(Intent.CATEGORY_HOME);
+        } else {
+            launcher = new ComponentName(context.getPackageName(), info.activityInfo.name);
+        }
+
         // Clear the packageName as system can fail to dedupe it b/64108432
         homeIntent.setComponent(launcher).setPackage(null);
     }
@@ -74,7 +83,7 @@
 
     public void onOverviewToggle() {
         getLauncher().runOnUiThread(() -> {
-                    if (DEBUG_START_FALLBACK_ACTIVITY) {
+                    if (isUsingFallbackActivity()) {
                         mContext.startActivity(new Intent(mContext, RecentsActivity.class)
                                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent
                                         .FLAG_ACTIVITY_CLEAR_TASK));
@@ -149,4 +158,15 @@
         return false;
     }
 
+    public boolean isUsingFallbackActivity() {
+        return DEBUG_START_FALLBACK_ACTIVITY;
+    }
+
+    public ActivityControlHelper getActivityControlHelper() {
+        if (DEBUG_START_FALLBACK_ACTIVITY) {
+            return new FallbackActivityControllerHelper();
+        } else {
+            return new LauncherActivityControllerHelper();
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/OverviewInteractionState.java b/quickstep/src/com/android/quickstep/OverviewInteractionState.java
index 4af89bf..522a883 100644
--- a/quickstep/src/com/android/quickstep/OverviewInteractionState.java
+++ b/quickstep/src/com/android/quickstep/OverviewInteractionState.java
@@ -15,20 +15,28 @@
  */
 package com.android.quickstep;
 
-import static com.android.quickstep.TouchInteractionService.DEBUG_SHOW_OVERVIEW_BUTTON;
+import static com.android.launcher3.Utilities.getPrefs;
+import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_QUICK_SCRUB;
+import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_SWIPE_UP;
 import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_HIDE_BACK_BUTTON;
 import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_SHOW_OVERVIEW_BUTTON;
 
 import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
+import android.support.annotation.WorkerThread;
 import android.util.Log;
 
+import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.util.UiThreadHelper;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 
+import java.util.concurrent.ExecutionException;
+
 /**
  * Sets overview interaction flags, such as:
  *
@@ -39,55 +47,109 @@
  *
  * @see com.android.systemui.shared.system.NavigationBarCompat.InteractionType and associated flags.
  */
-public class OverviewInteractionState {
+public class OverviewInteractionState implements OnSharedPreferenceChangeListener {
 
     private static final String TAG = "OverviewFlags";
-    private static final Handler sUiHandler = new Handler(Looper.getMainLooper()) {
-        @Override
-        public void handleMessage(Message msg) {
-            updateOverviewInteractionFlag((Context) msg.obj, msg.what, msg.arg1 == 1);
-        }
-    };
-    private static final Handler sBackgroundHandler = new Handler(
-            UiThreadHelper.getBackgroundLooper()) {
-        @Override
-        public void handleMessage(Message msg) {
-            ISystemUiProxy systemUiProxy = (ISystemUiProxy) msg.obj;
-            int flags = msg.what;
-            try {
-                systemUiProxy.setInteractionState(flags);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Unable to update overview interaction flags", e);
+
+    // We do not need any synchronization for this variable as its only written on UI thread.
+    private static OverviewInteractionState INSTANCE;
+
+    public static OverviewInteractionState getInstance(final Context context) {
+        if (INSTANCE == null) {
+            if (Looper.myLooper() == Looper.getMainLooper()) {
+                INSTANCE = new OverviewInteractionState(context.getApplicationContext());
+            } else {
+                try {
+                    return new MainThreadExecutor().submit(
+                            () -> OverviewInteractionState.getInstance(context)).get();
+                } catch (InterruptedException|ExecutionException e) {
+                    throw new RuntimeException(e);
+                }
             }
         }
-    };
-
-    private static int sFlags = DEBUG_SHOW_OVERVIEW_BUTTON ? FLAG_SHOW_OVERVIEW_BUTTON : 0;
-
-    public static void setBackButtonVisible(Context context, boolean visible) {
-        updateFlagOnUi(context, FLAG_HIDE_BACK_BUTTON, !visible);
+        return INSTANCE;
     }
 
-    private static void updateFlagOnUi(Context context, int flag, boolean enabled) {
-        sUiHandler.removeMessages(flag);
-        sUiHandler.sendMessage(sUiHandler.obtainMessage(flag, enabled ? 1 : 0, 0, context));
+    private static final String KEY_SWIPE_UP_ENABLED = "pref_enable_quickstep";
+
+    private static final int MSG_SET_PROXY = 200;
+    private static final int MSG_SET_BACK_BUTTON_VISIBLE = 201;
+    private static final int MSG_SET_SWIPE_UP_ENABLED = 202;
+
+    private final Handler mUiHandler;
+    private final Handler mBgHandler;
+
+    // These are updated on the background thread
+    private ISystemUiProxy mISystemUiProxy;
+    private boolean mBackButtonVisible = true;
+    private boolean mSwipeUpEnabled = true;
+
+    private OverviewInteractionState(Context context) {
+        mUiHandler = new Handler(this::handleUiMessage);
+        mBgHandler = new Handler(UiThreadHelper.getBackgroundLooper(), this::handleBgMessage);
+
+        SharedPreferences prefs = getPrefs(context);
+        prefs.registerOnSharedPreferenceChangeListener(this);
+        onSharedPreferenceChanged(prefs, KEY_SWIPE_UP_ENABLED);
     }
 
-    private static void updateOverviewInteractionFlag(Context context, int flag, boolean enabled) {
-        if (enabled) {
-            sFlags |= flag;
-        } else {
-            sFlags &= ~flag;
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences prefs, String s) {
+        if (KEY_SWIPE_UP_ENABLED.equals(s)) {
+            mUiHandler.removeMessages(MSG_SET_SWIPE_UP_ENABLED);
+            boolean swipeUpEnabled = prefs.getBoolean(s, true);
+            mUiHandler.obtainMessage(MSG_SET_SWIPE_UP_ENABLED,
+                    swipeUpEnabled ? 1 : 0, 0).sendToTarget();
         }
+    }
 
-        ISystemUiProxy systemUiProxy = RecentsModel.getInstance(context).getSystemUiProxy();
-        if (systemUiProxy == null) {
-            Log.w(TAG, "Unable to update overview interaction flags; not bound to service");
+    public void setBackButtonVisible(boolean visible) {
+        mUiHandler.removeMessages(MSG_SET_BACK_BUTTON_VISIBLE);
+        mUiHandler.obtainMessage(MSG_SET_BACK_BUTTON_VISIBLE, visible ? 1 : 0, 0)
+                .sendToTarget();
+    }
+
+    public void setSystemUiProxy(ISystemUiProxy proxy) {
+        mBgHandler.obtainMessage(MSG_SET_PROXY, proxy).sendToTarget();
+    }
+
+    private boolean handleUiMessage(Message msg) {
+        mBgHandler.obtainMessage(msg.what, msg.arg1, msg.arg2).sendToTarget();
+        return true;
+    }
+
+    private boolean handleBgMessage(Message msg) {
+        switch (msg.what) {
+            case MSG_SET_PROXY:
+                mISystemUiProxy = (ISystemUiProxy) msg.obj;
+                break;
+            case MSG_SET_BACK_BUTTON_VISIBLE:
+                mBackButtonVisible = msg.arg1 != 0;
+                break;
+            case MSG_SET_SWIPE_UP_ENABLED:
+                mSwipeUpEnabled = msg.arg1 != 0;
+                break;
+        }
+        applyFlags();
+        return true;
+    }
+
+    @WorkerThread
+    private void applyFlags() {
+        if (mISystemUiProxy == null) {
             return;
         }
-        // If we aren't already setting these flags, do so now on the background thread.
-        if (!sBackgroundHandler.hasMessages(sFlags)) {
-            sBackgroundHandler.sendMessage(sBackgroundHandler.obtainMessage(sFlags, systemUiProxy));
+
+        int flags;
+        if (mSwipeUpEnabled) {
+            flags = mBackButtonVisible ? 0 : FLAG_HIDE_BACK_BUTTON;
+        } else {
+            flags = FLAG_DISABLE_SWIPE_UP | FLAG_DISABLE_QUICK_SCRUB | FLAG_SHOW_OVERVIEW_BUTTON;
+        }
+        try {
+            mISystemUiProxy.setInteractionState(flags);
+        } catch (RemoteException e) {
+            Log.w(TAG, "Unable to update overview interaction flags", e);
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 1d443fd..12e1a2b 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -25,6 +25,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.badge.BadgeInfo;
+import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.views.BaseDragLayer;
 
 /**
@@ -49,6 +50,8 @@
         setContentView(R.layout.fallback_recents_activity);
         mRecentsRootView = findViewById(R.id.drag_layer);
         mFallbackRecentsView = findViewById(R.id.overview_panel);
+
+        RecentsActivityTracker.onRecentsActivityCreate(this);
     }
 
     @Override
@@ -57,6 +60,11 @@
     }
 
     @Override
+    public View getRootView() {
+        return mRecentsRootView;
+    }
+
+    @Override
     public <T extends View> T getOverviewPanel() {
         return (T) mFallbackRecentsView;
     }
@@ -73,4 +81,16 @@
 
     @Override
     public void invalidateParent(ItemInfo info) { }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        UiFactory.onStart(this);
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        super.onTrimMemory(level);
+        UiFactory.onTrimMemory(this, level);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsActivityTracker.java b/quickstep/src/com/android/quickstep/RecentsActivityTracker.java
new file mode 100644
index 0000000..6a82dc0
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RecentsActivityTracker.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
+
+import java.lang.ref.WeakReference;
+import java.util.function.BiPredicate;
+
+/**
+ * Utility class to track create/destroy for RecentsActivity
+ */
+@TargetApi(Build.VERSION_CODES.P)
+public class RecentsActivityTracker implements ActivityInitListener {
+
+    private static final Object LOCK = new Object();
+    private static WeakReference<RecentsActivityTracker> sTracker = new WeakReference<>(null);
+
+    private final BiPredicate<RecentsActivity, Boolean> mOnInitListener;
+
+    public RecentsActivityTracker(BiPredicate<RecentsActivity, Boolean> onInitListener) {
+        mOnInitListener = onInitListener;
+    }
+
+    @Override
+    public void register() {
+        synchronized (LOCK) {
+            sTracker = new WeakReference<>(this);
+        }
+    }
+
+    @Override
+    public void unregister() {
+        synchronized (LOCK) {
+            if (sTracker.get() == this) {
+                sTracker.clear();
+            }
+        }
+    }
+
+    public static void onRecentsActivityCreate(RecentsActivity activity) {
+        synchronized (LOCK) {
+            RecentsActivityTracker tracker = sTracker.get();
+            if (tracker != null && tracker.mOnInitListener.test(activity, false)) {
+                sTracker.clear();
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index ecd6c26..df7214e 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -47,7 +47,6 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.R;
-import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.recents.IOverviewProxy;
@@ -61,7 +60,6 @@
 @TargetApi(Build.VERSION_CODES.O)
 public class TouchInteractionService extends Service {
 
-    public static final boolean DEBUG_SHOW_OVERVIEW_BUTTON = false;
     public static final boolean DEBUG_OPEN_OVERVIEW_VIA_ALT_TAB = false;
 
     private static final SparseArray<String> sMotionEventNames;
@@ -107,9 +105,7 @@
             mRecentsModel.setSystemUiProxy(mISystemUiProxy);
             RemoteRunnable.executeSafely(() -> mISystemUiProxy.setRecentsOnboardingText(
                     getResources().getString(R.string.recents_swipe_up_onboarding)));
-            Launcher launcher = (Launcher) LauncherAppState.getInstance(
-                    TouchInteractionService.this).getModel().getCallback();
-            UiFactory.onLauncherStateOrFocusChanged(launcher);
+            mOverviewInteractionState.setSystemUiProxy(mISystemUiProxy);
         }
 
         @Override
@@ -174,6 +170,7 @@
     private MainThreadExecutor mMainThreadExecutor;
     private ISystemUiProxy mISystemUiProxy;
     private OverviewCommandHelper mOverviewCommandHelper;
+    private OverviewInteractionState mOverviewInteractionState;
 
     private Choreographer mMainThreadChoreographer;
     private Choreographer mBackgroundThreadChoreographer;
@@ -187,6 +184,7 @@
         mOverviewCommandHelper = new OverviewCommandHelper(this);
         mMainThreadChoreographer = Choreographer.getInstance();
         mEventQueue = new MotionEventQueue(mMainThreadChoreographer, mNoOpTouchConsumer);
+        mOverviewInteractionState = OverviewInteractionState.getInstance(this);
 
         sConnected = true;
 
@@ -235,7 +233,8 @@
                 tracker = VelocityTracker.obtain();
             }
             return new OtherActivityTouchConsumer(this, runningTaskInfo, mRecentsModel,
-                            mOverviewCommandHelper.homeIntent, mISystemUiProxy, mMainThreadExecutor,
+                            mOverviewCommandHelper.homeIntent,
+                            mOverviewCommandHelper.getActivityControlHelper(), mMainThreadExecutor,
                             mBackgroundThreadChoreographer, downHitTarget, tracker);
         }
     }
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index 060d680..2d2a483 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -15,22 +15,16 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.LauncherState.FAST_OVERVIEW;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
-import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
 import static com.android.quickstep.QuickScrubController.QUICK_SWITCH_START_DURATION;
 import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL;
 import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB;
 import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SWITCH;
 import static com.android.quickstep.TouchConsumer.isInteractionQuick;
-import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously;
+import static com.android.systemui.shared.recents.utilities.Utilities
+        .postAtFrontOfQueueAsynchronously;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
 
 import android.animation.Animator;
-import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager.RunningTaskInfo;
@@ -41,7 +35,6 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.metrics.LogMaker;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
@@ -53,25 +46,24 @@
 import android.view.ViewTreeObserver.OnDrawListener;
 
 import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.states.InternalStateHandler;
 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.launcher3.util.TraceHelper;
-import com.android.launcher3.util.ViewOnDrawExecutor;
+import com.android.quickstep.ActivityControlHelper.ActivityInitListener;
+import com.android.quickstep.ActivityControlHelper.LayoutListener;
 import com.android.quickstep.TouchConsumer.InteractionType;
+import com.android.quickstep.util.SysuiEventLogger;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -85,42 +77,8 @@
 
 import java.util.StringJoiner;
 
-class EventLogTags {
-    private EventLogTags() {
-    }  // don't instantiate
-
-    /** 524292 sysui_multi_action (content|4) */
-    public static final int SYSUI_MULTI_ACTION = 524292;
-
-    public static void writeSysuiMultiAction(Object[] content) {
-        android.util.EventLog.writeEvent(SYSUI_MULTI_ACTION, content);
-    }
-}
-
-class MetricsLogger {
-    private static MetricsLogger sMetricsLogger;
-
-    private static MetricsLogger getLogger() {
-        if (sMetricsLogger == null) {
-            sMetricsLogger = new MetricsLogger();
-        }
-        return sMetricsLogger;
-    }
-
-    protected void saveLog(Object[] rep) {
-        EventLogTags.writeSysuiMultiAction(rep);
-    }
-
-    public void write(LogMaker content) {
-        if (content.getType() == 0/*MetricsEvent.TYPE_UNKNOWN*/) {
-            content.setType(4/*MetricsEvent.TYPE_ACTION*/);
-        }
-        saveLog(content.serialize());
-    }
-}
-
 @TargetApi(Build.VERSION_CODES.O)
-public class WindowTransformSwipeHandler extends InternalStateHandler {
+public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> {
     private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName();
     private static final boolean DEBUG_STATES = false;
 
@@ -209,12 +167,14 @@
 
     private final Context mContext;
     private final int mRunningTaskId;
+    private final ActivityControlHelper<T> mActivityControlHelper;
+    private final ActivityInitListener mActivityInitListener;
 
     private MultiStateCallback mStateCallback;
     private AnimatorPlaybackController mLauncherTransitionController;
 
-    private Launcher mLauncher;
-    private LauncherLayoutListener mLauncherLayoutListener;
+    private T mActivity;
+    private LayoutListener mLayoutListener;
     private RecentsView mRecentsView;
     private QuickScrubController mQuickScrubController;
 
@@ -235,12 +195,16 @@
     private Matrix mTmpMatrix = new Matrix();
     private final long mTouchTimeMs;
     private long mLauncherFrameDrawnTime;
-    private final MetricsLogger mMetricsLogger = new MetricsLogger();
 
-    WindowTransformSwipeHandler(RunningTaskInfo runningTaskInfo, Context context, long touchTimeMs) {
+    WindowTransformSwipeHandler(RunningTaskInfo runningTaskInfo, Context context, long touchTimeMs,
+            ActivityControlHelper<T> controller) {
         mContext = context;
         mRunningTaskId = runningTaskInfo.id;
         mTouchTimeMs = touchTimeMs;
+        mActivityControlHelper = controller;
+        mActivityInitListener = mActivityControlHelper
+                .createActivityInitListener(this::onActivityInit);
+
         // Register the input consumer on the UI thread, to ensure that it runs after any pending
         // unregister calls
         mMainExecutor.execute(mInputConsumer::registerInputConsumer);
@@ -313,7 +277,8 @@
                 mSourceStackBounds.height() - mSourceInsets.bottom);
 
         Rect tempRect = new Rect();
-        RecentsView.getPageRect(dp, mContext, tempRect);
+        mTransitionDragLength = mActivityControlHelper
+                .getSwipeUpDestinationAndLength(dp, mContext, tempRect);
 
         mTargetRect.set(tempRect);
         mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left,
@@ -334,14 +299,6 @@
                 Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0),
                 Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0));
         mSourceRect.set(scaledTargetRect);
-
-        Rect targetInsets = dp.getInsets();
-        if (dp.isVerticalBarLayout()) {
-            int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
-            mTransitionDragLength = dp.hotseatBarSizePx + dp.hotseatBarSidePaddingPx + hotseatInset;
-        } else {
-            mTransitionDragLength = dp.heightPx - tempRect.bottom;
-        }
     }
 
     private long getFadeInDuration() {
@@ -356,40 +313,39 @@
         }
     }
 
-    @Override
-    protected boolean init(final Launcher launcher, boolean alreadyOnHome) {
-        if (launcher == mLauncher) {
+    public void initWhenReady() {
+        mActivityInitListener.register();
+    }
+
+    private boolean onActivityInit(final T activity, Boolean alreadyOnHome) {
+        if (mActivity == activity) {
             return true;
         }
-        if (mLauncher != null) {
+        if (mActivity != null) {
             // The launcher may have been recreated as a result of device rotation.
             int oldState = mStateCallback.getState() & ~LAUNCHER_UI_STATES;
             initStateCallbacks();
             mStateCallback.setState(oldState);
-            mLauncherLayoutListener.setHandler(null);
+            mLayoutListener.setHandler(null);
         }
         mWasLauncherAlreadyVisible = alreadyOnHome;
-        mLauncher = launcher;
+        mActivity = activity;
 
-        // For the duration of the gesture, lock the screen orientation to ensure that we do not
-        // rotate mid-quickscrub
-        mLauncher.getRotationHelper().setStateHandlerRequest(REQUEST_LOCK);
-
-        mRecentsView = mLauncher.getOverviewPanel();
+        mRecentsView = activity.getOverviewPanel();
         mQuickScrubController = mRecentsView.getQuickScrubController();
-        mLauncherLayoutListener = new LauncherLayoutListener(mLauncher);
+        mLayoutListener = mActivityControlHelper.createLayoutListener(mActivity);
 
         mStateCallback.setState(STATE_LAUNCHER_PRESENT);
         if (alreadyOnHome) {
-            onLauncherStart(launcher);
+            onLauncherStart(activity);
         } else {
-            launcher.setOnStartCallback(this::onLauncherStart);
+            activity.setOnStartCallback(this::onLauncherStart);
         }
         return true;
     }
 
-    private void onLauncherStart(final Launcher launcher) {
-        if (mLauncher != launcher) {
+    private void onLauncherStart(final T activity) {
+        if (mActivity != activity) {
             return;
         }
         if ((mStateCallback.getState() & STATE_HANDLER_INVALIDATED) != 0) {
@@ -397,31 +353,21 @@
         }
 
         mStateCallback.setState(STATE_LAUNCHER_STARTED);
-        LauncherState startState = mLauncher.getStateManager().getState();
-        if (startState.disableRestore) {
-            startState = mLauncher.getStateManager().getRestState();
-        }
-        mLauncher.getStateManager().setRestState(startState);
-
-        AbstractFloatingView.closeAllOpenViews(mLauncher, mWasLauncherAlreadyVisible);
+        mActivityControlHelper.prepareRecentsUI(mActivity, mWasLauncherAlreadyVisible);
+        AbstractFloatingView.closeAllOpenViews(activity, mWasLauncherAlreadyVisible);
 
 
-        if (mWasLauncherAlreadyVisible && !mLauncher.getAppTransitionManager().isAnimating()) {
-            DeviceProfile dp = mLauncher.getDeviceProfile();
-            long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx);
-            mLauncherTransitionController = mLauncher.getStateManager()
-                    .createAnimationToNewWorkspace(OVERVIEW, accuracy);
+        if (mWasLauncherAlreadyVisible) {
+            mLauncherTransitionController = mActivityControlHelper
+                    .createControllerForVisibleActivity(activity);
             mLauncherTransitionController.dispatchOnStart();
             mLauncherTransitionController.setPlayFraction(mCurrentShift.value);
 
             mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_DRAWN);
         } else {
             TraceHelper.beginSection("WTS-init");
-            mLauncher.getStateManager().goToState(OVERVIEW, false);
-            TraceHelper.partitionSection("WTS-init", "State changed");
-
             // TODO: Implement a better animation for fading in
-            View rootView = mLauncher.getRootView();
+            View rootView = activity.getRootView();
             rootView.setAlpha(0);
             rootView.getViewTreeObserver().addOnDrawListener(new OnDrawListener() {
 
@@ -430,21 +376,18 @@
                     TraceHelper.endSection("WTS-init", "Launcher frame is drawn");
                     rootView.post(() ->
                             rootView.getViewTreeObserver().removeOnDrawListener(this));
-                    if (launcher != mLauncher) {
+                    if (activity != mActivity) {
                         return;
                     }
 
                     mStateCallback.setState(STATE_LAUNCHER_DRAWN);
                 }
             });
-
-            // Optimization, hide the all apps view to prevent layout while initializing
-            mLauncher.getAppsView().setVisibility(View.GONE);
         }
 
         mRecentsView.showTask(mRunningTaskId);
         mRecentsView.setFirstTaskIconScaledDown(true /* isScaledDown */, false /* animate */);
-        mLauncherLayoutListener.open();
+        mLayoutListener.open();
     }
 
     public void setLauncherOnDrawCallback(Runnable callback) {
@@ -452,7 +395,7 @@
     }
 
     private void launcherFrameDrawn() {
-        View rootView = mLauncher.getRootView();
+        View rootView = mActivity.getRootView();
         if (rootView.getAlpha() < 1) {
             if (mGestureStarted) {
                 final MultiStateCallback callback = mStateCallback;
@@ -471,19 +414,12 @@
     }
 
     private void initializeLauncherAnimationController() {
-        mLauncherLayoutListener.setHandler(this);
+        mLayoutListener.setHandler(this);
         onLauncherLayoutChanged();
 
         final long transitionDelay = mLauncherFrameDrawnTime - mTouchTimeMs;
-        // Mimic ActivityMetricsLogger.logAppTransitionMultiEvents() logging for
-        // "Recents" activity for app transition tests for the app-to-recents case.
-        final LogMaker builder = new LogMaker(761/*APP_TRANSITION*/);
-        builder.setPackageName("com.android.systemui");
-        builder.addTaggedData(871/*FIELD_CLASS_NAME*/,
-                "com.android.systemui.recents.RecentsActivity");
-        builder.addTaggedData(319/*APP_TRANSITION_DELAY_MS*/,
-                transitionDelay);
-        mMetricsLogger.write(builder);
+        SysuiEventLogger.writeDummyRecentsTransition(transitionDelay);
+
         if (LatencyTrackerCompat.isEnabled(mContext)) {
             LatencyTrackerCompat.logToggleRecents((int) transitionDelay);
         }
@@ -508,7 +444,7 @@
     }
 
     private void onQuickInteractionStart() {
-        mLauncher.getStateManager().goToState(FAST_OVERVIEW,
+        mActivityControlHelper.onQuickInteractionStart(mActivity,
                 mWasLauncherAlreadyVisible || mGestureStarted);
         mQuickScrubController.onQuickScrubStart(false);
     }
@@ -523,32 +459,14 @@
     }
 
     /**
-     * Called by {@link #mLauncherLayoutListener} when launcher layout changes
+     * Called by {@link #mLayoutListener} when launcher layout changes
      */
     public void onLauncherLayoutChanged() {
-        initTransitionEndpoints(mLauncher.getDeviceProfile());
+        initTransitionEndpoints(mActivity.getDeviceProfile());
 
         if (!mWasLauncherAlreadyVisible) {
-            float startProgress;
-            AllAppsTransitionController controller = mLauncher.getAllAppsController();
-
-            if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
-                startProgress = 1;
-            } else {
-                float scrollRange = Math.max(controller.getShiftRange(), 1);
-                startProgress = (mTransitionDragLength / scrollRange) + 1;
-            }
-            AnimatorSet anim = new AnimatorSet();
-            ObjectAnimator shiftAnim = ObjectAnimator.ofFloat(controller, ALL_APPS_PROGRESS,
-                    startProgress, OVERVIEW.getVerticalProgress(mLauncher));
-            shiftAnim.setInterpolator(LINEAR);
-            anim.play(shiftAnim);
-
-            // TODO: Link this animation to state animation, so that it is cancelled
-            // automatically on state change
-            anim.setDuration(mTransitionDragLength * 2);
-            mLauncherTransitionController =
-                    AnimatorPlaybackController.wrap(anim, mTransitionDragLength * 2);
+            mLauncherTransitionController = mActivityControlHelper
+                    .createControllerForHiddenActivity(mActivity, mTransitionDragLength);
             mLauncherTransitionController.setPlayFraction(mCurrentShift.value);
         }
     }
@@ -661,14 +579,13 @@
                 }
             }
         }
-
         mRecentsAnimationWrapper.setController(controller, apps);
         setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
     }
 
     public void onRecentsAnimationCanceled() {
         mRecentsAnimationWrapper.setController(null, null);
-        clearReference();
+        mActivityInitListener.unregister();
         setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
     }
 
@@ -684,9 +601,10 @@
      * on both background and UI threads
      */
     private void notifyGestureStarted() {
-        final Launcher curLauncher = mLauncher;
-        if (curLauncher != null) {
-            curLauncher.onQuickstepGestureStarted(mWasLauncherAlreadyVisible);
+        final T curActivity = mActivity;
+        if (curActivity != null) {
+            mActivityControlHelper.onQuickstepGestureStarted(
+                    curActivity, mWasLauncherAlreadyVisible);
         }
     }
 
@@ -770,25 +688,20 @@
             mGestureEndCallback.run();
         }
 
-        clearReference();
+        mActivityInitListener.unregister();
         mInputConsumer.unregisterInputConsumer();
     }
 
     private void invalidateHandlerWithLauncher() {
         mLauncherTransitionController = null;
-        mLauncherLayoutListener.setHandler(null);
-        mLauncherLayoutListener.close(false);
-
-        // Restore the requested orientation to the user preference after the gesture has ended
-        mLauncher.getRotationHelper().setStateHandlerRequest(REQUEST_NONE);
+        mLayoutListener.finish();
 
         mRecentsView.setFirstTaskIconScaledDown(false /* isScaledDown */, false /* animate */);
     }
 
     private void resetStateForAnimationCancel() {
-        LauncherState startState = mLauncher.getStateManager().getRestState();
-        boolean animate = mWasLauncherAlreadyVisible || mGestureStarted;
-        mLauncher.getStateManager().goToState(startState, animate);
+        boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted;
+        mActivityControlHelper.onTransitionCancelled(mActivity, wasVisible);
     }
 
     public void layoutListenerClosed() {
@@ -817,17 +730,8 @@
                         if (taskView != null) {
                             // Defer finishing the animation until the next launcher frame with the
                             // new thumbnail
-                            ViewOnDrawExecutor executor = new ViewOnDrawExecutor() {
-                                @Override
-                                public void onViewDetachedFromWindow(View v) {
-                                    if (!isCompleted()) {
-                                        runAllTasks();
-                                    }
-                                }
-                            };
-                            executor.attachTo(mLauncher, taskView,
-                                    false /* waitForLoadAnimation */);
-                            executor.execute(finishTransitionRunnable);
+                            mActivityControlHelper.executeOnNextDraw(mActivity, taskView,
+                                    finishTransitionRunnable);
                             finishTransitionPosted = true;
                         }
                     }
@@ -843,8 +747,7 @@
     }
 
     private void setupLauncherUiAfterSwipeUpAnimation() {
-        // Re apply state in case we did something funky during the transition.
-        mLauncher.getStateManager().reapplyState();
+        mActivityControlHelper.onSwipeUpComplete(mActivity);
 
         // Animate the first icon.
         mRecentsView.setFirstTaskIconScaledDown(false /* isScaledDown */, true /* animate */);
diff --git a/quickstep/src/com/android/quickstep/util/SysuiEventLogger.java b/quickstep/src/com/android/quickstep/util/SysuiEventLogger.java
new file mode 100644
index 0000000..d474ded
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/SysuiEventLogger.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import android.metrics.LogMaker;
+import android.util.EventLog;
+
+/**
+ * Utility class for writing logs on behalf of systemUI
+ */
+public class SysuiEventLogger {
+
+    /** 524292 sysui_multi_action (content|4) */
+    public static final int SYSUI_MULTI_ACTION = 524292;
+
+    private static void write(LogMaker content) {
+        if (content.getType() == 0/*MetricsEvent.TYPE_UNKNOWN*/) {
+            content.setType(4/*MetricsEvent.TYPE_ACTION*/);
+        }
+        EventLog.writeEvent(SYSUI_MULTI_ACTION, content.serialize());
+    }
+
+    public static void writeDummyRecentsTransition(long transitionDelay) {
+        // Mimic ActivityMetricsLogger.logAppTransitionMultiEvents() logging for
+        // "Recents" activity for app transition tests for the app-to-recents case.
+        final LogMaker builder = new LogMaker(761/*APP_TRANSITION*/);
+        builder.setPackageName("com.android.systemui");
+        builder.addTaggedData(871/*FIELD_CLASS_NAME*/,
+                "com.android.systemui.recents.RecentsActivity");
+        builder.addTaggedData(319/*APP_TRANSITION_DELAY_MS*/,
+                transitionDelay);
+        write(builder);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/LauncherLayoutListener.java b/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java
similarity index 83%
rename from quickstep/src/com/android/quickstep/LauncherLayoutListener.java
rename to quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java
index fbdbe7a..6b7143d 100644
--- a/quickstep/src/com/android/quickstep/LauncherLayoutListener.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java
@@ -13,7 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.quickstep;
+package com.android.quickstep.views;
+
+import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
 
 import android.graphics.Rect;
 import android.view.MotionEvent;
@@ -21,11 +23,14 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.Launcher;
+import com.android.quickstep.ActivityControlHelper.LayoutListener;
+import com.android.quickstep.WindowTransformSwipeHandler;
 
 /**
  * Floating view which shows the task snapshot allowing it to be dragged and placed.
  */
-public class LauncherLayoutListener extends AbstractFloatingView implements Insettable {
+public class LauncherLayoutListener extends AbstractFloatingView
+        implements Insettable, LayoutListener {
 
     private final Launcher mLauncher;
     private WindowTransformSwipeHandler mHandler;
@@ -36,6 +41,7 @@
         setVisibility(INVISIBLE);
     }
 
+    @Override
     public void setHandler(WindowTransformSwipeHandler handler) {
         mHandler = handler;
     }
@@ -65,6 +71,7 @@
         }
     }
 
+    @Override
     public void open() {
         if (!mIsOpen) {
             mLauncher.getDragLayer().addView(this);
@@ -86,4 +93,11 @@
     protected boolean isOfType(int type) {
         return (type & TYPE_QUICKSTEP_PREVIEW) != 0;
     }
+
+    @Override
+    public void finish() {
+        setHandler(null);
+        close(false);
+        mLauncher.getRotationHelper().setStateHandlerRequest(REQUEST_NONE);
+    }
 }
diff --git a/res/layout/launcher.xml b/res/layout/launcher.xml
index d07ff81..4693917 100644
--- a/res/layout/launcher.xml
+++ b/res/layout/launcher.xml
@@ -44,13 +44,6 @@
             layout="@layout/overview_panel"
             android:visibility="gone" />
 
-        <!-- DO NOT CHANGE THE ID -->
-        <include
-            android:id="@+id/hotseat"
-            layout="@layout/hotseat"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent" />
-
         <!-- Keep these behind the workspace so that they are not visible when
          we go into AllApps -->
         <com.android.launcher3.pageindicators.WorkspacePageIndicator
@@ -69,6 +62,13 @@
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:visibility="invisible" />
+
+        <!-- DO NOT CHANGE THE ID -->
+        <include
+            android:id="@+id/hotseat"
+            layout="@layout/hotseat"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
     </com.android.launcher3.dragndrop.DragLayer>
 
 </com.android.launcher3.LauncherRootView>
diff --git a/res/layout/widgets_list_row_view.xml b/res/layout/widgets_list_row_view.xml
index 91baf7a..eec57a5 100644
--- a/res/layout/widgets_list_row_view.xml
+++ b/res/layout/widgets_list_row_view.xml
@@ -31,7 +31,6 @@
         android:layout_height="@dimen/widget_section_height"
         android:background="?android:attr/colorPrimary"
         android:drawablePadding="@dimen/widget_section_horizontal_padding"
-        android:ellipsize="end"
         android:focusable="true"
         android:gravity="start|center_vertical"
         android:paddingBottom="@dimen/widget_section_vertical_padding"
diff --git a/res/values/styles.xml b/res/values/styles.xml
index ac6a6b1..8076c80 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -112,7 +112,6 @@
         <item name="android:focusable">true</item>
         <item name="android:gravity">center_horizontal</item>
         <item name="android:singleLine">true</item>
-        <item name="android:ellipsize">marquee</item>
         <item name="android:textColor">?android:attr/textColorSecondary</item>
         <item name="android:fontFamily">sans-serif-condensed</item>
 
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 35edaf4..458f7b2 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -55,6 +55,8 @@
     private ActionMode mCurrentActionMode;
     protected boolean mIsSafeModeEnabled;
 
+    private OnStartCallback mOnStartCallback;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -85,6 +87,8 @@
 
     public abstract <T extends View> T getOverviewPanel();
 
+    public abstract View getRootView();
+
     public abstract BadgeInfo getBadgeInfoForItem(ItemInfo info);
 
     public abstract void invalidateParent(ItemInfo info);
@@ -188,4 +192,26 @@
     protected boolean onErrorStartingShortcut(Intent intent, ItemInfo info) {
         return false;
     }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+
+        if (mOnStartCallback != null) {
+            mOnStartCallback.onActivityStart(this);
+            mOnStartCallback = null;
+        }
+    }
+
+    public <T extends BaseDraggingActivity> void setOnStartCallback(OnStartCallback<T> callback) {
+        mOnStartCallback = callback;
+    }
+
+    /**
+     * Callback for listening for onStart
+     */
+    public interface OnStartCallback<T extends BaseDraggingActivity> {
+
+        void onActivityStart(T activity);
+    }
 }
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index fc61155..41bfcb7 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -28,6 +28,7 @@
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.support.v4.graphics.ColorUtils;
+import android.text.TextUtils.TruncateAt;
 import android.util.AttributeSet;
 import android.util.Property;
 import android.util.TypedValue;
@@ -163,10 +164,18 @@
         mLongPressHelper = new CheckLongPressHelper(this);
         mStylusEventHelper = new StylusEventHelper(new SimpleOnStylusPressListener(this), this);
 
+        setEllipsize(TruncateAt.END);
         setAccessibilityDelegate(mActivity.getAccessibilityDelegate());
 
     }
 
+    @Override
+    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+        // Disable marques when not focused to that, so that updating text does not cause relayout.
+        setEllipsize(focused ? TruncateAt.MARQUEE : TruncateAt.END);
+        super.onFocusChanged(focused, direction, previouslyFocusedRect);
+    }
+
     /**
      * Resets the view so it can be recycled.
      */
@@ -419,7 +428,7 @@
         }
     }
 
-    private void setTextAlpha(int alpha) {
+    public void setTextAlpha(int alpha) {
         super.setTextColor(ColorUtils.setAlphaComponent(mTextColor, alpha));
     }
 
@@ -521,31 +530,30 @@
      * Sets the icon for this view based on the layout direction.
      */
     private void setIcon(Drawable icon) {
-        mIcon = icon;
-        mIcon.setBounds(0, 0, mIconSize, mIconSize);
         if (mIsIconVisible) {
-            applyCompoundDrawables(mIcon);
+            applyCompoundDrawables(icon);
         }
+        mIcon = icon;
     }
 
     public void setIconVisible(boolean visible) {
         mIsIconVisible = visible;
-        mDisableRelayout = true;
-        Drawable icon = mIcon;
-        if (!visible) {
-            icon = new ColorDrawable(Color.TRANSPARENT);
-            icon.setBounds(0, 0, mIconSize, mIconSize);
-        }
+        Drawable icon = visible ? mIcon : new ColorDrawable(Color.TRANSPARENT);
         applyCompoundDrawables(icon);
-        mDisableRelayout = false;
     }
 
     protected void applyCompoundDrawables(Drawable icon) {
+        // If we had already set an icon before, disable relayout as the icon size is the
+        // same as before.
+        mDisableRelayout = mIcon != null;
+
+        icon.setBounds(0, 0, mIconSize, mIconSize);
         if (mLayoutHorizontal) {
             setCompoundDrawablesRelative(icon, null, null, null);
         } else {
             setCompoundDrawables(null, icon, null, null);
         }
+        mDisableRelayout = false;
     }
 
     @Override
diff --git a/src/com/android/launcher3/DropTargetBar.java b/src/com/android/launcher3/DropTargetBar.java
index a3fe89a..dec6cb4 100644
--- a/src/com/android/launcher3/DropTargetBar.java
+++ b/src/com/android/launcher3/DropTargetBar.java
@@ -16,10 +16,10 @@
 
 package com.android.launcher3;
 
-import static com.android.launcher3.AlphaUpdateListener.updateVisibility;
 import static com.android.launcher3.ButtonDropTarget.TOOLTIP_DEFAULT;
 import static com.android.launcher3.ButtonDropTarget.TOOLTIP_LEFT;
 import static com.android.launcher3.ButtonDropTarget.TOOLTIP_RIGHT;
+import static com.android.launcher3.anim.AlphaUpdateListener.updateVisibility;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
 
 import android.animation.TimeInterpolator;
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index a38ce07..e779b5e 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -209,7 +209,6 @@
 
     @Thunk boolean mWorkspaceLoading = true;
 
-    private OnStartCallback mOnStartCallback;
     private OnResumeCallback mOnResumeCallback;
 
     private ViewOnDrawExecutor mPendingExecutor;
@@ -365,6 +364,9 @@
             getRootView().dispatchInsets();
             getStateManager().reapplyState();
 
+            // Recreate touch controllers
+            mDragLayer.setup(mDragController);
+
             // TODO: We can probably avoid rebind when only screen size changed.
             rebindModel();
         }
@@ -401,10 +403,6 @@
         return mStateManager;
     }
 
-    public LauncherAppTransitionManager getAppTransitionManager() {
-        return mAppTransitionManager;
-    }
-
     protected void overrideTheme(boolean isDark, boolean supportsDarkText) {
         if (isDark) {
             setTheme(R.style.LauncherThemeDark);
@@ -767,10 +765,6 @@
         super.onStart();
         FirstFrameAnimatorHelper.setIsVisible(true);
 
-        if (mOnStartCallback != null) {
-            mOnStartCallback.onLauncherStart(this);
-            mOnStartCallback = null;
-        }
         if (mLauncherCallbacks != null) {
             mLauncherCallbacks.onStart();
         }
@@ -961,7 +955,7 @@
         mDragController.setMoveTarget(mWorkspace);
         mDropTargetBar.setup(mDragController);
 
-        mAllAppsController.setupViews(mAppsView, mHotseat);
+        mAllAppsController.setupViews(mAppsView);
     }
 
     /**
@@ -1177,6 +1171,7 @@
         return mAllAppsController;
     }
 
+    @Override
     public LauncherRootView getRootView() {
         return (LauncherRootView) mLauncherView;
     }
@@ -1258,11 +1253,15 @@
                 // In all these cases, only animate if we're already on home
                 AbstractFloatingView.closeAllOpenViews(this, isStarted());
 
-                mStateManager.goToState(NORMAL);
+                if (!isInState(NORMAL)) {
+                    // Only change state, if not already the same. This prevents cancelling any
+                    // animations running as part of resume
+                    mStateManager.goToState(NORMAL);
+                }
 
                 // Reset the apps view
                 if (!alreadyOnHome && mAppsView != null) {
-                    mAppsView.reset();
+                    mAppsView.reset(isStarted() /* animate */);
                 }
 
                 if (shouldMoveToDefaultScreen && !mWorkspace.isTouchActive()) {
@@ -1775,10 +1774,6 @@
         mOnResumeCallback = callback;
     }
 
-    public void setOnStartCallback(OnStartCallback callback) {
-        mOnStartCallback = callback;
-    }
-
     /**
      * Implementation of the method from LauncherModel.Callbacks.
      */
@@ -2450,12 +2445,4 @@
 
         void onLauncherResume();
     }
-
-    /**
-     * Callback for listening for onStart
-     */
-    public interface OnStartCallback {
-
-        void onLauncherStart(Launcher launcher);
-    }
 }
diff --git a/src/com/android/launcher3/LauncherAppTransitionManager.java b/src/com/android/launcher3/LauncherAppTransitionManager.java
index 19fa3d4..04f9b3a 100644
--- a/src/com/android/launcher3/LauncherAppTransitionManager.java
+++ b/src/com/android/launcher3/LauncherAppTransitionManager.java
@@ -62,13 +62,4 @@
     public ActivityOptions getActivityLaunchOptions(Launcher launcher, View v) {
         return getDefaultActivityLaunchOptions(launcher, v);
     }
-
-    /** Cancels the current Launcher transition animation */
-    public void finishLauncherAnimation() {
-    }
-
-    public boolean isAnimating() {
-        // We don't know when the activity options are being used.
-        return false;
-    }
 }
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index acad901..8b7ba20 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -60,7 +60,7 @@
 import com.android.launcher3.provider.LauncherDbUtils;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.provider.RestoreDbTask;
-import com.android.launcher3.util.NoLocaleSqliteContext;
+import com.android.launcher3.util.NoLocaleSQLiteHelper;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.Thunk;
 
@@ -546,7 +546,7 @@
     /**
      * The class is subclassed in tests to create an in-memory db.
      */
-    public static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
+    public static class DatabaseHelper extends NoLocaleSQLiteHelper implements LayoutParserCallback {
         private final Handler mWidgetHostResetHandler;
         private final Context mContext;
         private long mMaxItemId = -1;
@@ -572,7 +572,7 @@
          */
         public DatabaseHelper(
                 Context context, Handler widgetHostResetHandler, String tableName) {
-            super(new NoLocaleSqliteContext(context), tableName, null, SCHEMA_VERSION);
+            super(context, tableName, SCHEMA_VERSION);
             mContext = context;
             mWidgetHostResetHandler = widgetHostResetHandler;
         }
diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java
index fc4de2d..b1273b6 100644
--- a/src/com/android/launcher3/LauncherRootView.java
+++ b/src/com/android/launcher3/LauncherRootView.java
@@ -29,6 +29,7 @@
     private int mRightInsetBarWidth;
 
     private View mAlignedView;
+    private WindowStateListener mWindowStateListener;
 
     public LauncherRootView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -117,4 +118,31 @@
             }
         }
     }
+
+    public void setWindowStateListener(WindowStateListener listener) {
+        mWindowStateListener = listener;
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+        if (mWindowStateListener != null) {
+            mWindowStateListener.onWindowFocusChanged(hasWindowFocus);
+        }
+    }
+
+    @Override
+    protected void onWindowVisibilityChanged(int visibility) {
+        super.onWindowVisibilityChanged(visibility);
+        if (mWindowStateListener != null) {
+            mWindowStateListener.onWindowVisibilityChanged(visibility);
+        }
+    }
+
+    public interface WindowStateListener {
+
+        void onWindowFocusChanged(boolean hasFocus);
+
+        void onWindowVisibilityChanged(int visibility);
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index e5d8f47..9fef64a 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -40,6 +40,16 @@
  */
 public class LauncherState {
 
+
+    /**
+     * Set of elements indicating various workspace elements which change visibility across states
+     * Note that workspace is not included here as in that case, we animate individual pages
+     */
+    public static final int NONE = 0;
+    public static final int HOTSEAT = 1 << 0;
+    public static final int ALL_APPS_HEADER = 1 << 1;
+    public static final int ALL_APPS_CONTENT = 1 << 2;
+
     protected static final int FLAG_SHOW_SCRIM = 1 << 0;
     protected static final int FLAG_MULTI_PAGE = 1 << 1;
     protected static final int FLAG_DISABLE_ACCESSIBILITY = 1 << 2;
@@ -51,7 +61,6 @@
     protected static final int FLAG_DISABLE_INTERACTION = 1 << 8;
     protected static final int FLAG_OVERVIEW_UI = 1 << 9;
 
-
     protected static final PageAlphaProvider DEFAULT_ALPHA_PROVIDER =
             new PageAlphaProvider(ACCEL_2) {
                 @Override
@@ -68,13 +77,13 @@
     public static final LauncherState NORMAL = new LauncherState(0, ContainerType.WORKSPACE,
             0, FLAG_DISABLE_RESTORE | FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED);
 
-    public static final LauncherState ALL_APPS = new AllAppsState(1);
-
-    public static final LauncherState SPRING_LOADED = new SpringLoadedState(2);
-
-    public static final LauncherState OVERVIEW = new OverviewState(3);
-
-    public static final LauncherState FAST_OVERVIEW = new FastOverviewState(4);
+    /**
+     * Various Launcher states arranged in the increasing order of UI layers
+     */
+    public static final LauncherState SPRING_LOADED = new SpringLoadedState(1);
+    public static final LauncherState OVERVIEW = new OverviewState(2);
+    public static final LauncherState FAST_OVERVIEW = new FastOverviewState(3);
+    public static final LauncherState ALL_APPS = new AllAppsState(4);
 
     public final int ordinal;
 
@@ -161,10 +170,6 @@
         return new float[] {1, 0, 0};
     }
 
-    public float getHoseatAlpha(Launcher launcher) {
-        return 1f;
-    }
-
     public float getOverviewTranslationX(Launcher launcher) {
         return launcher.getDragLayer().getMeasuredWidth();
     }
@@ -179,6 +184,10 @@
         return launcher.getWorkspace();
     }
 
+    public int getVisibleElements(Launcher launcher) {
+        return HOTSEAT;
+    }
+
     /**
      * Fraction shift in the vertical translation UI and related properties
      *
diff --git a/src/com/android/launcher3/LauncherStateManager.java b/src/com/android/launcher3/LauncherStateManager.java
index 950a8ac..7d50a52 100644
--- a/src/com/android/launcher3/LauncherStateManager.java
+++ b/src/com/android/launcher3/LauncherStateManager.java
@@ -17,6 +17,7 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -28,8 +29,12 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.anim.PropertySetter.AnimatedPropertySetter;
 import com.android.launcher3.uioverrides.UiFactory;
 
+import java.util.ArrayList;
+
 /**
  * TODO: figure out what kind of tests we can write for this
  *
@@ -78,6 +83,7 @@
     private final AnimationConfig mConfig = new AnimationConfig();
     private final Handler mUiHandler;
     private final Launcher mLauncher;
+    private final ArrayList<StateListener> mListeners = new ArrayList<>();
 
     private StateHandler[] mStateHandlers;
     private LauncherState mState = NORMAL;
@@ -87,8 +93,6 @@
 
     private LauncherState mRestState;
 
-    private StateListener mStateListener;
-
     public LauncherStateManager(Launcher l) {
         mUiHandler = new Handler(Looper.getMainLooper());
         mLauncher = l;
@@ -105,8 +109,12 @@
         return mStateHandlers;
     }
 
-    public void setStateListener(StateListener stateListener) {
-        mStateListener = stateListener;
+    public void addStateListener(StateListener listener) {
+        mListeners.add(listener);
+    }
+
+    public void removeStateListener(StateListener listener) {
+        mListeners.remove(listener);
     }
 
     /**
@@ -183,13 +191,13 @@
         mConfig.reset();
 
         if (!animated) {
-            preOnStateTransitionStart();
             onStateTransitionStart(state);
             for (StateHandler handler : getStateHandlers()) {
                 handler.setState(state);
             }
-            if (mStateListener != null) {
-                mStateListener.onStateSetImmediately(state);
+
+            for (int i = mListeners.size() - 1; i >= 0; i--) {
+                mListeners.get(i).onStateSetImmediately(state);
             }
             onStateTransitionEnd(state);
 
@@ -237,7 +245,6 @@
 
     protected AnimatorSet createAnimationToNewWorkspaceInternal(final LauncherState state,
             AnimatorSetBuilder builder, final Runnable onCompleteRunnable) {
-        preOnStateTransitionStart();
 
         for (StateHandler handler : getStateHandlers()) {
             builder.startTag(handler);
@@ -251,16 +258,16 @@
             public void onAnimationStart(Animator animation) {
                 // Change the internal state only when the transition actually starts
                 onStateTransitionStart(state);
-                if (mStateListener != null) {
-                    mStateListener.onStateTransitionStart(state);
+                for (int i = mListeners.size() - 1; i >= 0; i--) {
+                    mListeners.get(i).onStateTransitionStart(state);
                 }
             }
 
             @Override
             public void onAnimationEnd(Animator animation) {
                 super.onAnimationEnd(animation);
-                if (mStateListener != null) {
-                    mStateListener.onStateTransitionComplete(mState);
+                for (int i = mListeners.size() - 1; i >= 0; i--) {
+                    mListeners.get(i).onStateTransitionComplete(state);
                 }
             }
 
@@ -277,15 +284,6 @@
         return mConfig.mCurrentAnimation;
     }
 
-    private void preOnStateTransitionStart() {
-        // If we are still animating to launcher from an app,
-        // finish it and let this state animation take over.
-        LauncherAppTransitionManager transitionManager = mLauncher.getAppTransitionManager();
-        if (transitionManager != null) {
-            transitionManager.finishLauncherAnimation();
-        }
-    }
-
     private void onStateTransitionStart(LauncherState state) {
         mState.onStateDisabled(mLauncher);
         mState = state;
@@ -351,6 +349,15 @@
         mConfig.reset();
     }
 
+    /**
+     * Sets the animation as the current state animation, i.e., canceled when
+     * starting another animation and may block some launcher interactions while running.
+     */
+    public void setCurrentAnimation(AnimatorSet anim) {
+        cancelAnimation();
+        mConfig.setAnimation(anim);
+    }
+
     private class StartAnimRunnable implements Runnable {
 
         private final AnimatorSet mAnim;
@@ -376,12 +383,14 @@
     public static class AnimationConfig extends AnimatorListenerAdapter {
         public long duration;
         public boolean userControlled;
+        private PropertySetter mProperSetter;
 
         private AnimatorSet mCurrentAnimation;
 
         public void reset() {
             duration = 0;
             userControlled = false;
+            mProperSetter = null;
 
             if (mCurrentAnimation != null) {
                 mCurrentAnimation.setDuration(0);
@@ -390,6 +399,14 @@
             }
         }
 
+        public PropertySetter getProperSetter(AnimatorSetBuilder builder) {
+            if (mProperSetter == null) {
+                mProperSetter = duration == 0 ? NO_ANIM_PROPERTY_SETTER
+                        : new AnimatedPropertySetter(duration, builder);
+            }
+            return mProperSetter;
+        }
+
         @Override
         public void onAnimationEnd(Animator animation) {
             if (mCurrentAnimation == animation) {
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index f6d0248..63c1181 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -18,6 +18,8 @@
 
 import static com.android.launcher3.LauncherAnimUtils.DRAWABLE_ALPHA;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherState.HOTSEAT;
+import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
 
 import android.animation.Animator;
@@ -32,65 +34,14 @@
 import com.android.launcher3.LauncherStateManager.AnimationConfig;
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.graphics.ViewScrim;
 
 /**
- * A convenience class to update a view's visibility state after an alpha animation.
- */
-class AlphaUpdateListener extends AnimatorListenerAdapter implements ValueAnimator.AnimatorUpdateListener {
-    private static final float ALPHA_CUTOFF_THRESHOLD = 0.01f;
-
-    private View mView;
-    private boolean mAccessibilityEnabled;
-    private boolean mCanceled = false;
-
-    public AlphaUpdateListener(View v, boolean accessibilityEnabled) {
-        mView = v;
-        mAccessibilityEnabled = accessibilityEnabled;
-    }
-
-    @Override
-    public void onAnimationUpdate(ValueAnimator arg0) {
-        updateVisibility(mView, mAccessibilityEnabled);
-    }
-
-    public static void updateVisibility(View view, boolean accessibilityEnabled) {
-        // We want to avoid the extra layout pass by setting the views to GONE unless
-        // accessibility is on, in which case not setting them to GONE causes a glitch.
-        int invisibleState = accessibilityEnabled ? View.GONE : View.INVISIBLE;
-        if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != invisibleState) {
-            view.setVisibility(invisibleState);
-        } else if (view.getAlpha() > ALPHA_CUTOFF_THRESHOLD
-                && view.getVisibility() != View.VISIBLE) {
-            view.setVisibility(View.VISIBLE);
-        }
-    }
-
-    @Override
-    public void onAnimationCancel(Animator animation) {
-        mCanceled = true;
-    }
-
-    @Override
-    public void onAnimationEnd(Animator arg0) {
-        if (mCanceled) return;
-        updateVisibility(mView, mAccessibilityEnabled);
-    }
-
-    @Override
-    public void onAnimationStart(Animator arg0) {
-        // We want the views to be visible for animation, so fade-in/out is visible
-        mView.setVisibility(View.VISIBLE);
-    }
-}
-
-/**
  * Manages the animations between each of the workspace states.
  */
 public class WorkspaceStateTransitionAnimation {
 
-    public static final PropertySetter NO_ANIM_PROPERTY_SETTER = new PropertySetter();
-
     private final Launcher mLauncher;
     private final Workspace mWorkspace;
 
@@ -107,9 +58,7 @@
 
     public void setStateWithAnimation(LauncherState toState, AnimatorSetBuilder builder,
             AnimationConfig config) {
-        AnimatedPropertySetter propertySetter =
-                new AnimatedPropertySetter(config.duration, builder);
-        setWorkspaceProperty(toState, propertySetter);
+        setWorkspaceProperty(toState, config.getProperSetter(builder));
     }
 
     public float getFinalScale() {
@@ -135,10 +84,12 @@
         propertySetter.setFloat(mWorkspace, View.TRANSLATION_Y,
                 scaleAndTranslation[2], Interpolators.ZOOM_IN);
 
-        propertySetter.setViewAlpha(mLauncher.getHotseat(), state.getHoseatAlpha(mLauncher),
+        int elements = state.getVisibleElements(mLauncher);
+        float hotseatAlpha = (elements & HOTSEAT) != 0 ? 1 : 0;
+        propertySetter.setViewAlpha(mLauncher.getHotseat(), hotseatAlpha,
                 pageAlphaProvider.interpolator);
         propertySetter.setViewAlpha(mLauncher.getWorkspace().getPageIndicator(),
-                state.getHoseatAlpha(mLauncher), pageAlphaProvider.interpolator);
+                hotseatAlpha, pageAlphaProvider.interpolator);
 
         // Set scrim
         propertySetter.setFloat(ViewScrim.get(mWorkspace), ViewScrim.PROGRESS,
@@ -162,71 +113,4 @@
         propertySetter.setFloat(cl.getShortcutsAndWidgets(), View.ALPHA,
                 pageAlpha, pageAlphaProvider.interpolator);
     }
-
-    public static class PropertySetter {
-
-        public void setViewAlpha(View view, float alpha, TimeInterpolator interpolator) {
-            view.setAlpha(alpha);
-            AlphaUpdateListener.updateVisibility(view, isAccessibilityEnabled(view.getContext()));
-        }
-
-        public <T> void setFloat(T target, Property<T, Float> property, float value,
-                TimeInterpolator interpolator) {
-            property.set(target, value);
-        }
-
-        public <T> void setInt(T target, Property<T, Integer> property, int value,
-                TimeInterpolator interpolator) {
-            property.set(target, value);
-        }
-    }
-
-    public static class AnimatedPropertySetter extends PropertySetter {
-
-        private final long mDuration;
-        private final AnimatorSetBuilder mStateAnimator;
-
-        public AnimatedPropertySetter(long duration, AnimatorSetBuilder builder) {
-            mDuration = duration;
-            mStateAnimator = builder;
-        }
-
-        @Override
-        public void setViewAlpha(View view, float alpha, TimeInterpolator interpolator) {
-            if (view.getAlpha() == alpha) {
-                return;
-            }
-            ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.ALPHA, alpha);
-            anim.addListener(new AlphaUpdateListener(
-                    view, isAccessibilityEnabled(view.getContext())));
-            anim.setDuration(mDuration).setInterpolator(interpolator);
-            mStateAnimator.play(anim);
-        }
-
-        @Override
-        public <T> void setFloat(T target, Property<T, Float> property, float value,
-                TimeInterpolator interpolator) {
-            if (property.get(target) == value) {
-                return;
-            }
-            Animator anim = ObjectAnimator.ofFloat(target, property, value);
-            anim.setDuration(mDuration).setInterpolator(interpolator);
-            mStateAnimator.play(anim);
-        }
-
-        @Override
-        public <T> void setInt(T target, Property<T, Integer> property, int value,
-                TimeInterpolator interpolator) {
-            if (property.get(target) == value) {
-                return;
-            }
-            Animator anim = ObjectAnimator.ofInt(target, property, value);
-            anim.setDuration(mDuration).setInterpolator(interpolator);
-            mStateAnimator.play(anim);
-        }
-
-        private TimeInterpolator getFadeInterpolator(float finalAlpha) {
-            return finalAlpha == 0 ? Interpolators.DEACCEL_2 : null;
-        }
-    }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 39a8df3..8f5fcf5 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -217,14 +217,14 @@
     /**
      * Resets the state of AllApps.
      */
-    public void reset() {
+    public void reset(boolean animate) {
         for (int i = 0; i < mAH.length; i++) {
             if (mAH[i].recyclerView != null) {
                 mAH[i].recyclerView.scrollToTop();
             }
         }
         if (isHeaderVisible()) {
-            mHeader.reset();
+            mHeader.reset(animate);
         }
         // Reset the search bar and base recycler view after transitioning home
         mSearchUiManager.resetSearch();
@@ -360,7 +360,7 @@
 
     public void onTabChanged(int pos) {
         mHeader.setMainActive(pos == 0);
-        reset();
+        reset(true /* animate */);
         if (mAH[pos].recyclerView != null) {
             mAH[pos].recyclerView.bindFastScrollbar();
 
@@ -383,6 +383,18 @@
         return mHeader;
     }
 
+    public View getSearchView() {
+        return mSearchContainer;
+    }
+
+    public View getContentView() {
+        return mViewPager == null ? getActiveRecyclerView() : mViewPager;
+    }
+
+    public RecyclerViewFastScroller getScrollBar() {
+        return getActiveRecyclerView().getScrollbar();
+    }
+
     public void setupHeader() {
         mHeader.setVisibility(View.VISIBLE);
         mHeader.setup(mAH, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView == null);
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 13a42f1..bf8f531 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -1,7 +1,10 @@
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT;
+import static com.android.launcher3.LauncherState.ALL_APPS_HEADER;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS;
 
 import android.animation.Animator;
@@ -13,16 +16,15 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
-import com.android.launcher3.Hotseat;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager.AnimationConfig;
 import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.SearchUiManager.OnScrollRangeChangeListener;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.util.Themes;
 
 /**
@@ -55,7 +57,6 @@
     public static final float PARALLAX_COEFFICIENT = .125f;
 
     private AllAppsContainerView mAppsView;
-    private Hotseat mHotseat;
 
     private final Launcher mLauncher;
     private final boolean mIsDarkTheme;
@@ -88,7 +89,6 @@
 
     private void onProgressAnimationStart() {
         // Initialize values that should not change until #onDragEnd
-        mHotseat.setVisibility(View.VISIBLE);
         mAppsView.setVisibility(View.VISIBLE);
     }
 
@@ -116,14 +116,10 @@
         mProgress = progress;
         float shiftCurrent = progress * mShiftRange;
 
-        float workspaceHotseatAlpha = Utilities.boundToRange(progress, 0f, 1f);
-        float alpha = 1 - workspaceHotseatAlpha;
-
         mAppsView.setTranslationY(shiftCurrent);
         float hotseatTranslation = -mShiftRange + shiftCurrent;
 
         if (!mIsVerticalLayout) {
-            mAppsView.setAlpha(alpha);
             mLauncher.getHotseat().setTranslationY(hotseatTranslation);
             mLauncher.getWorkspace().getPageIndicator().setTranslationY(hotseatTranslation);
         }
@@ -149,6 +145,7 @@
     @Override
     public void setState(LauncherState state) {
         setProgress(state.getVerticalProgress(mLauncher));
+        setAlphas(state, NO_ANIM_PROPERTY_SETTER);
         onProgressAnimationEnd();
     }
 
@@ -161,6 +158,7 @@
             AnimatorSetBuilder builder, AnimationConfig config) {
         float targetProgress = toState.getVerticalProgress(mLauncher);
         if (Float.compare(mProgress, targetProgress) == 0) {
+            setAlphas(toState, config.getProperSetter(builder));
             // Fail fast
             onProgressAnimationEnd();
             return;
@@ -174,6 +172,19 @@
         anim.addListener(getProgressAnimatorListener());
 
         builder.play(anim);
+
+        setAlphas(toState, config.getProperSetter(builder));
+    }
+
+    private void setAlphas(LauncherState toState, PropertySetter setter) {
+        int visibleElements = toState.getVisibleElements(mLauncher);
+        boolean hasHeader = (visibleElements & ALL_APPS_HEADER) != 0;
+        boolean hasContent = (visibleElements & ALL_APPS_CONTENT) != 0;
+
+        setter.setViewAlpha(mAppsView.getSearchView(), hasHeader ? 1 : 0, LINEAR);
+        setter.setViewAlpha(mAppsView.getContentView(), hasContent ? 1 : 0, LINEAR);
+        setter.setViewAlpha(mAppsView.getScrollBar(), hasContent ? 1 : 0, LINEAR);
+        mAppsView.getFloatingHeaderView().setContentVisibility(hasHeader, hasContent, setter);
     }
 
     public AnimatorListenerAdapter getProgressAnimatorListener() {
@@ -190,10 +201,8 @@
         };
     }
 
-    public void setupViews(AllAppsContainerView appsView, Hotseat hotseat) {
+    public void setupViews(AllAppsContainerView appsView) {
         mAppsView = appsView;
-        mHotseat = hotseat;
-        mHotseat.bringToFront();
         mAppsView.getSearchUiManager().addOnScrollRangeChangeListener(this);
     }
 
@@ -210,15 +219,12 @@
     private void onProgressAnimationEnd() {
         if (Float.compare(mProgress, 1f) == 0) {
             mAppsView.setVisibility(View.INVISIBLE);
-            mHotseat.setVisibility(View.VISIBLE);
-            mAppsView.reset();
+            mAppsView.reset(false /* animate */);
         } else if (Float.compare(mProgress, 0f) == 0) {
-            mHotseat.setVisibility(View.INVISIBLE);
             mAppsView.setVisibility(View.VISIBLE);
             mAppsView.onScrollUpEnd();
         } else {
             mAppsView.setVisibility(View.VISIBLE);
-            mHotseat.setVisibility(View.VISIBLE);
         }
     }
 }
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java
index a0dc5a3..461f5b5 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderView.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.Point;
@@ -29,6 +31,7 @@
 import android.widget.LinearLayout;
 
 import com.android.launcher3.R;
+import com.android.launcher3.anim.PropertySetter;
 
 public class FloatingHeaderView extends LinearLayout implements
         ValueAnimator.AnimatorUpdateListener {
@@ -57,7 +60,7 @@
         }
     };
 
-    private ViewGroup mTabLayout;
+    protected ViewGroup mTabLayout;
     private AllAppsRecyclerView mMainRV;
     private AllAppsRecyclerView mWorkRV;
     private AllAppsRecyclerView mCurrentRV;
@@ -65,6 +68,8 @@
     private boolean mHeaderCollapsed;
     private int mSnappedScrolledY;
     private int mTranslationY;
+
+    private boolean mAllowTouchForwarding;
     private boolean mForwardToRecyclerView;
 
     protected boolean mTabsHidden;
@@ -91,7 +96,7 @@
         mWorkRV = setupRV(mWorkRV, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView);
         mParent = (ViewGroup) mMainRV.getParent();
         setMainActive(true);
-        reset();
+        reset(false);
     }
 
     private AllAppsRecyclerView setupRV(AllAppsRecyclerView old, AllAppsRecyclerView updated) {
@@ -158,12 +163,19 @@
         }
     }
 
-    public void reset() {
-        int translateTo = 0;
-        mAnimator.setIntValues(mTranslationY, translateTo);
-        mAnimator.addUpdateListener(this);
-        mAnimator.setDuration(150);
-        mAnimator.start();
+    public void reset(boolean animate) {
+        if (mAnimator.isStarted()) {
+            mAnimator.cancel();
+        }
+        if (animate) {
+            mAnimator.setIntValues(mTranslationY, 0);
+            mAnimator.addUpdateListener(this);
+            mAnimator.setDuration(150);
+            mAnimator.start();
+        } else {
+            mTranslationY = 0;
+            apply();
+        }
         mHeaderCollapsed = false;
         mSnappedScrolledY = -mMaxTranslation;
         mCurrentRV.scrollToTop();
@@ -181,6 +193,10 @@
 
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (!mAllowTouchForwarding) {
+            mForwardToRecyclerView = false;
+            return super.onInterceptTouchEvent(ev);
+        }
         calcOffset(mTempOffset);
         ev.offsetLocation(mTempOffset.x, mTempOffset.y);
         mForwardToRecyclerView = mCurrentRV.onInterceptTouchEvent(ev);
@@ -208,6 +224,19 @@
         p.x = getLeft() - mCurrentRV.getLeft() - mParent.getLeft();
         p.y = getTop() - mCurrentRV.getTop() - mParent.getTop();
     }
+
+    public void setContentVisibility(boolean hasHeader, boolean hasContent, PropertySetter setter) {
+        setter.setViewAlpha(this, hasContent ? 1 : 0, LINEAR);
+        allowTouchForwarding(hasContent);
+    }
+
+    protected void allowTouchForwarding(boolean allow) {
+        mAllowTouchForwarding = allow;
+    }
+
+    public boolean hasVisibleContent() {
+        return false;
+    }
 }
 
 
diff --git a/src/com/android/launcher3/anim/AlphaUpdateListener.java b/src/com/android/launcher3/anim/AlphaUpdateListener.java
new file mode 100644
index 0000000..04d97a7
--- /dev/null
+++ b/src/com/android/launcher3/anim/AlphaUpdateListener.java
@@ -0,0 +1,73 @@
+/*
+ * 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.launcher3.anim;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.view.View;
+
+/**
+ * A convenience class to update a view's visibility state after an alpha animation.
+ */
+public class AlphaUpdateListener extends AnimatorListenerAdapter implements AnimatorUpdateListener {
+    private static final float ALPHA_CUTOFF_THRESHOLD = 0.01f;
+
+    private View mView;
+    private boolean mAccessibilityEnabled;
+    private boolean mCanceled = false;
+
+    public AlphaUpdateListener(View v, boolean accessibilityEnabled) {
+        mView = v;
+        mAccessibilityEnabled = accessibilityEnabled;
+    }
+
+    @Override
+    public void onAnimationUpdate(ValueAnimator arg0) {
+        updateVisibility(mView, mAccessibilityEnabled);
+    }
+
+    public static void updateVisibility(View view, boolean accessibilityEnabled) {
+        // We want to avoid the extra layout pass by setting the views to GONE unless
+        // accessibility is on, in which case not setting them to GONE causes a glitch.
+        int invisibleState = accessibilityEnabled ? View.GONE : View.INVISIBLE;
+        if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != invisibleState) {
+            view.setVisibility(invisibleState);
+        } else if (view.getAlpha() > ALPHA_CUTOFF_THRESHOLD
+                && view.getVisibility() != View.VISIBLE) {
+            view.setVisibility(View.VISIBLE);
+        }
+    }
+
+    @Override
+    public void onAnimationCancel(Animator animation) {
+        mCanceled = true;
+    }
+
+    @Override
+    public void onAnimationEnd(Animator arg0) {
+        if (mCanceled) return;
+        updateVisibility(mView, mAccessibilityEnabled);
+    }
+
+    @Override
+    public void onAnimationStart(Animator arg0) {
+        // We want the views to be visible for animation, so fade-in/out is visible
+        mView.setVisibility(View.VISIBLE);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/launcher3/anim/PropertySetter.java b/src/com/android/launcher3/anim/PropertySetter.java
new file mode 100644
index 0000000..51580b1
--- /dev/null
+++ b/src/com/android/launcher3/anim/PropertySetter.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2016 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.launcher3.anim;
+
+import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.util.Property;
+import android.view.View;
+
+/**
+ * Utility class for setting a property with or without animation
+ */
+public class PropertySetter {
+
+    public static final PropertySetter NO_ANIM_PROPERTY_SETTER = new PropertySetter();
+
+    public void setViewAlpha(View view, float alpha, TimeInterpolator interpolator) {
+        view.setAlpha(alpha);
+        AlphaUpdateListener.updateVisibility(view, isAccessibilityEnabled(view.getContext()));
+    }
+
+    public <T> void setFloat(T target, Property<T, Float> property, float value,
+            TimeInterpolator interpolator) {
+        property.set(target, value);
+    }
+
+    public <T> void setInt(T target, Property<T, Integer> property, int value,
+            TimeInterpolator interpolator) {
+        property.set(target, value);
+    }
+
+    public static class AnimatedPropertySetter extends PropertySetter {
+
+        private final long mDuration;
+        private final AnimatorSetBuilder mStateAnimator;
+
+        public AnimatedPropertySetter(long duration, AnimatorSetBuilder builder) {
+            mDuration = duration;
+            mStateAnimator = builder;
+        }
+
+        @Override
+        public void setViewAlpha(View view, float alpha, TimeInterpolator interpolator) {
+            if (view.getAlpha() == alpha) {
+                return;
+            }
+            ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.ALPHA, alpha);
+            anim.addListener(new AlphaUpdateListener(
+                    view, isAccessibilityEnabled(view.getContext())));
+            anim.setDuration(mDuration).setInterpolator(interpolator);
+            mStateAnimator.play(anim);
+        }
+
+        @Override
+        public <T> void setFloat(T target, Property<T, Float> property, float value,
+                TimeInterpolator interpolator) {
+            if (property.get(target) == value) {
+                return;
+            }
+            Animator anim = ObjectAnimator.ofFloat(target, property, value);
+            anim.setDuration(mDuration).setInterpolator(interpolator);
+            mStateAnimator.play(anim);
+        }
+
+        @Override
+        public <T> void setInt(T target, Property<T, Integer> property, int value,
+                TimeInterpolator interpolator) {
+            if (property.get(target) == value) {
+                return;
+            }
+            Animator anim = ObjectAnimator.ofInt(target, property, value);
+            anim.setDuration(mDuration).setInterpolator(interpolator);
+            mStateAnimator.play(anim);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index 28645dc..78ea419 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -51,6 +51,4 @@
 
     // When enabled shows a work profile tab in all apps
     public static final boolean ALL_APPS_TABS_ENABLED = true;
-
-    public static final boolean ENABLE_TWO_SWIPE_TARGETS = true;
 }
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
new file mode 100644
index 0000000..db53634
--- /dev/null
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.touch;
+
+import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.Utilities;
+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.util.TouchController;
+
+/**
+ * TouchController for handling state changes
+ */
+public abstract class AbstractStateChangeTouchController extends AnimatorListenerAdapter
+        implements TouchController, SwipeDetector.Listener {
+
+    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;
+
+    protected final Launcher mLauncher;
+    protected final SwipeDetector mDetector;
+
+    private boolean mNoIntercept;
+    protected int mStartContainerType;
+
+    protected LauncherState mFromState;
+    protected LauncherState mToState;
+    protected AnimatorPlaybackController mCurrentAnimation;
+
+    private float mStartProgress;
+    // Ratio of transition process [0, 1] to drag displacement (px)
+    private float mProgressMultiplier;
+
+    public AbstractStateChangeTouchController(Launcher l, SwipeDetector.Direction dir) {
+        mLauncher = l;
+        mDetector = new SwipeDetector(l, this, dir);
+    }
+
+    protected abstract boolean canInterceptTouch(MotionEvent ev);
+
+    /**
+     * Initializes the {@code mFromState} and {@code mToState} and swipe direction to use for
+     * the detector. In can of disabling swipe, return 0.
+     */
+    protected abstract int getSwipeDirection(MotionEvent ev);
+
+    @Override
+    public final boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            mNoIntercept = !canInterceptTouch(ev);
+            if (mNoIntercept) {
+                return false;
+            }
+
+            // Now figure out which direction scroll events the controller will start
+            // calling the callbacks.
+            final int directionsToDetectScroll;
+            boolean ignoreSlopWhenSettling = false;
+
+            if (mCurrentAnimation != null) {
+                if (mCurrentAnimation.getProgressFraction() > 1 - RECATCH_REJECTION_FRACTION) {
+                    directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
+                } else if (mCurrentAnimation.getProgressFraction() < RECATCH_REJECTION_FRACTION ) {
+                    directionsToDetectScroll = SwipeDetector.DIRECTION_NEGATIVE;
+                } else {
+                    directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
+                    ignoreSlopWhenSettling = true;
+                }
+            } else {
+                directionsToDetectScroll = getSwipeDirection(ev);
+                if (directionsToDetectScroll == 0) {
+                    mNoIntercept = true;
+                    return false;
+                }
+            }
+            mDetector.setDetectableScrollConditions(
+                    directionsToDetectScroll, ignoreSlopWhenSettling);
+        }
+
+        if (mNoIntercept) {
+            return false;
+        }
+
+        onControllerTouchEvent(ev);
+        return mDetector.isDraggingOrSettling();
+    }
+
+    @Override
+    public final boolean onControllerTouchEvent(MotionEvent ev) {
+        return mDetector.onTouchEvent(ev);
+    }
+
+    protected float getShiftRange() {
+        return mLauncher.getAllAppsController().getShiftRange();
+    }
+
+    protected abstract float initCurrentAnimation();
+
+    @Override
+    public void onDragStart(boolean start) {
+        if (mCurrentAnimation == null) {
+            mStartProgress = 0;
+            mProgressMultiplier = initCurrentAnimation();
+
+            mCurrentAnimation.getTarget().addListener(this);
+            mCurrentAnimation.dispatchOnStart();
+        } else {
+            mCurrentAnimation.pause();
+            mStartProgress = mCurrentAnimation.getProgressFraction();
+        }
+    }
+
+    @Override
+    public boolean onDrag(float displacement, float velocity) {
+        float deltaProgress = mProgressMultiplier * displacement;
+        mCurrentAnimation.setPlayFraction(deltaProgress + mStartProgress);
+        return true;
+    }
+
+    @Override
+    public void onDragEnd(float velocity, boolean fling) {
+        final int logAction;
+        final LauncherState targetState;
+        final float progress = mCurrentAnimation.getProgressFraction();
+
+        if (fling) {
+            logAction = Touch.FLING;
+            targetState =
+                    Float.compare(Math.signum(velocity), Math.signum(mProgressMultiplier)) == 0
+                            ? mToState : mFromState;
+            // snap to top or bottom using the release velocity
+        } else {
+            logAction = Touch.SWIPE;
+            targetState = (progress > SUCCESS_TRANSITION_PROGRESS) ? mToState : mFromState;
+        }
+
+
+        final float endProgress;
+        final float startProgress;
+        final long duration;
+
+        if (targetState == mToState) {
+            endProgress = 1;
+            if (progress >= 1) {
+                duration = 0;
+                startProgress = 1;
+            } else {
+                startProgress = Utilities.boundToRange(
+                        progress + velocity * SINGLE_FRAME_MS / getShiftRange(), 0f, 1f);
+                duration = SwipeDetector.calculateDuration(velocity,
+                        endProgress - Math.max(progress, 0));
+            }
+        } else {
+            endProgress = 0;
+            if (progress <= 0) {
+                duration = 0;
+                startProgress = 0;
+            } else {
+                startProgress = Utilities.boundToRange(
+                        progress + velocity * SINGLE_FRAME_MS / getShiftRange(), 0f, 1f);
+                duration = SwipeDetector.calculateDuration(velocity,
+                        Math.min(progress, 1) - endProgress);
+            }
+        }
+
+        mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState, logAction));
+        ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
+        anim.setFloatValues(startProgress, endProgress);
+        anim.setDuration(duration).setInterpolator(scrollInterpolatorForVelocity(velocity));
+        anim.start();
+    }
+
+    protected int getDirectionForLog() {
+        return mToState.ordinal > mFromState.ordinal ? Direction.UP : Direction.DOWN;
+    }
+
+    protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
+        if (targetState != mFromState) {
+            // Transition complete. log the action
+            mLauncher.getUserEventDispatcher().logStateChangeAction(logAction,
+                    getDirectionForLog(),
+                    mStartContainerType,
+                    mFromState.containerType,
+                    mToState.containerType,
+                    mLauncher.getWorkspace().getCurrentPage());
+        }
+        clearState();
+        mLauncher.getStateManager().goToState(targetState, false /* animated */);
+    }
+
+    protected void clearState() {
+        mCurrentAnimation = null;
+        mDetector.finishedScrolling();
+    }
+
+    @Override
+    public void onAnimationCancel(Animator animation) {
+        if (mCurrentAnimation != null && animation == mCurrentAnimation.getOriginalTarget()) {
+            Log.e(TAG, "Who dare cancel the animation when I am in control", new Exception());
+            clearState();
+        }
+    }
+}
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index f10a695..6f012f6 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -18,6 +18,7 @@
 import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
 
+import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 
@@ -78,8 +79,8 @@
         Launcher launcher = Launcher.getLauncher(v.getContext());
         if (!canStartDrag(launcher)) return false;
         // When we have exited all apps or are in transition, disregard long clicks
-        if (!launcher.isInState(LauncherState.ALL_APPS) ||
-                launcher.getWorkspace().isSwitchingState()) return false;
+        if (!launcher.isInState(ALL_APPS) && !launcher.isInState(OVERVIEW)) return false;
+        if (launcher.getWorkspace().isSwitchingState()) return false;
 
         // Start the drag
         final DragController dragController = launcher.getDragController();
diff --git a/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java b/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java
new file mode 100644
index 0000000..05a7d27
--- /dev/null
+++ b/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java
@@ -0,0 +1,56 @@
+/*
+ * 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.launcher3.util;
+
+import static android.database.sqlite.SQLiteDatabase.NO_LOCALIZED_COLLATORS;
+
+import static com.android.launcher3.Utilities.ATLEAST_P;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.database.DatabaseErrorHandler;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.database.sqlite.SQLiteDatabase.OpenParams;
+import android.database.sqlite.SQLiteOpenHelper;
+
+/**
+ * Extension of {@link SQLiteOpenHelper} which avoids creating default locale table by
+ * A context wrapper which creates databases without support for localized collators.
+ */
+public abstract class NoLocaleSQLiteHelper extends SQLiteOpenHelper {
+
+    public NoLocaleSQLiteHelper(Context context, String name, int version) {
+        super(ATLEAST_P ? context : new NoLocalContext(context), name, null, version);
+        if (ATLEAST_P) {
+            setOpenParams(new OpenParams.Builder().addOpenFlags(NO_LOCALIZED_COLLATORS).build());
+        }
+    }
+
+    private static class NoLocalContext extends ContextWrapper {
+        public NoLocalContext(Context base) {
+            super(base);
+        }
+
+        @Override
+        public SQLiteDatabase openOrCreateDatabase(
+                String name, int mode, CursorFactory factory, DatabaseErrorHandler errorHandler) {
+            return super.openOrCreateDatabase(
+                    name, mode | Context.MODE_NO_LOCALIZED_COLLATORS, factory, errorHandler);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/NoLocaleSqliteContext.java b/src/com/android/launcher3/util/NoLocaleSqliteContext.java
deleted file mode 100644
index c8a5ffb..0000000
--- a/src/com/android/launcher3/util/NoLocaleSqliteContext.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.android.launcher3.util;
-
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.database.DatabaseErrorHandler;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteDatabase.CursorFactory;
-
-/**
- * A context wrapper which creates databases without support for localized collators.
- */
-public class NoLocaleSqliteContext extends ContextWrapper {
-
-    public NoLocaleSqliteContext(Context context) {
-        super(context);
-    }
-
-    @Override
-    public SQLiteDatabase openOrCreateDatabase(
-            String name, int mode, CursorFactory factory, DatabaseErrorHandler errorHandler) {
-        return super.openOrCreateDatabase(
-                name, mode | Context.MODE_NO_LOCALIZED_COLLATORS, factory, errorHandler);
-    }
-}
diff --git a/src/com/android/launcher3/util/SQLiteCacheHelper.java b/src/com/android/launcher3/util/SQLiteCacheHelper.java
index 9084bfb..44c1762 100644
--- a/src/com/android/launcher3/util/SQLiteCacheHelper.java
+++ b/src/com/android/launcher3/util/SQLiteCacheHelper.java
@@ -92,10 +92,10 @@
     /**
      * A private inner class to prevent direct DB access.
      */
-    private class MySQLiteOpenHelper extends SQLiteOpenHelper {
+    private class MySQLiteOpenHelper extends NoLocaleSQLiteHelper {
 
         public MySQLiteOpenHelper(Context context, String name, int version) {
-            super(new NoLocaleSqliteContext(context), name, null, version);
+            super(context, name, version);
         }
 
         @Override
diff --git a/src/com/android/launcher3/util/VerticalSwipeController.java b/src/com/android/launcher3/util/VerticalSwipeController.java
deleted file mode 100644
index ae5bfd5..0000000
--- a/src/com/android/launcher3/util/VerticalSwipeController.java
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * Copyright (C) 2017 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.launcher3.util;
-
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.util.Log;
-import android.view.MotionEvent;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.touch.SwipeDetector;
-import com.android.launcher3.touch.SwipeDetector.Direction;
-
-
-/**
- * Handles vertical touch gesture on the DragLayer allowing transitioning from
- * {@link #mBaseState} to {@link LauncherState#ALL_APPS} and vice-versa.
- */
-public abstract class VerticalSwipeController extends AnimatorListenerAdapter
-        implements TouchController, SwipeDetector.Listener {
-
-    private static final String TAG = "VerticalSwipeController";
-
-    private static final float RECATCH_REJECTION_FRACTION = .0875f;
-    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;
-
-    protected final Launcher mLauncher;
-    protected final SwipeDetector mDetector;
-    private final LauncherState mBaseState;
-    private final LauncherState mTargetState;
-
-    private boolean mNoIntercept;
-
-    private AnimatorPlaybackController mCurrentAnimation;
-    protected LauncherState mToState;
-
-    private float mStartProgress;
-    // Ratio of transition process [0, 1] to drag displacement (px)
-    private float mProgressMultiplier;
-
-    public VerticalSwipeController(Launcher l, LauncherState baseState) {
-        this(l, baseState, ALL_APPS, SwipeDetector.VERTICAL);
-    }
-
-    public VerticalSwipeController(
-            Launcher l, LauncherState baseState, LauncherState targetState, Direction dir) {
-        mLauncher = l;
-        mDetector = new SwipeDetector(l, this, dir);
-        mBaseState = baseState;
-        mTargetState = targetState;
-    }
-
-    private boolean canInterceptTouch(MotionEvent ev) {
-        if (mCurrentAnimation != null) {
-            // If we are already animating from a previous state, we can intercept.
-            return true;
-        }
-        if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
-            return false;
-        }
-        return shouldInterceptTouch(ev);
-    }
-
-    protected abstract boolean shouldInterceptTouch(MotionEvent ev);
-
-    @Override
-    public void onAnimationCancel(Animator animation) {
-        if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) {
-            Log.e(TAG, "Who dare cancel the animation when I am in control", new Exception());
-            mDetector.finishedScrolling();
-            mCurrentAnimation = null;
-        }
-    }
-
-    @Override
-    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            mNoIntercept = !canInterceptTouch(ev);
-            if (mNoIntercept) {
-                return false;
-            }
-
-            // Now figure out which direction scroll events the controller will start
-            // calling the callbacks.
-            final int directionsToDetectScroll;
-            boolean ignoreSlopWhenSettling = false;
-
-            if (mCurrentAnimation != null) {
-                if (mCurrentAnimation.getProgressFraction() > 1 - RECATCH_REJECTION_FRACTION) {
-                    directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
-                } else if (mCurrentAnimation.getProgressFraction() < RECATCH_REJECTION_FRACTION ) {
-                    directionsToDetectScroll = SwipeDetector.DIRECTION_NEGATIVE;
-                } else {
-                    directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
-                    ignoreSlopWhenSettling = true;
-                }
-            } else {
-                directionsToDetectScroll = getSwipeDirection(ev);
-            }
-
-            mDetector.setDetectableScrollConditions(
-                    directionsToDetectScroll, ignoreSlopWhenSettling);
-        }
-
-        if (mNoIntercept) {
-            return false;
-        }
-
-        onControllerTouchEvent(ev);
-        return mDetector.isDraggingOrSettling();
-    }
-
-    protected abstract int getSwipeDirection(MotionEvent ev);
-
-    @Override
-    public boolean onControllerTouchEvent(MotionEvent ev) {
-        return mDetector.onTouchEvent(ev);
-    }
-
-    @Override
-    public void onDragStart(boolean start) {
-        if (mCurrentAnimation == null) {
-            float range = getShiftRange();
-            long maxAccuracy = (long) (2 * range);
-
-            // Build current animation
-            mToState = mLauncher.isInState(mTargetState) ? mBaseState : mTargetState;
-            mCurrentAnimation = mLauncher.getStateManager()
-                    .createAnimationToNewWorkspace(mToState, maxAccuracy);
-            mCurrentAnimation.getTarget().addListener(this);
-            mStartProgress = 0;
-            mProgressMultiplier =
-                    (mLauncher.isInState(mTargetState) ^ isTransitionFlipped() ? 1 : -1) / range;
-            mCurrentAnimation.dispatchOnStart();
-        } else {
-            mCurrentAnimation.pause();
-            mStartProgress = mCurrentAnimation.getProgressFraction();
-        }
-    }
-
-    protected boolean isTransitionFlipped() {
-        return false;
-    }
-
-    protected float getShiftRange() {
-        return mLauncher.getAllAppsController().getShiftRange();
-    }
-
-    @Override
-    public boolean onDrag(float displacement, float velocity) {
-        float deltaProgress = mProgressMultiplier * displacement;
-        mCurrentAnimation.setPlayFraction(deltaProgress + mStartProgress);
-        return true;
-    }
-
-    @Override
-    public void onDragEnd(float velocity, boolean fling) {
-        final long animationDuration;
-        final LauncherState targetState;
-        final float progress = mCurrentAnimation.getProgressFraction();
-
-        if (fling) {
-            if (velocity < 0 ^ isTransitionFlipped()) {
-                targetState = mTargetState;
-            } else {
-                targetState = mBaseState;
-            }
-            animationDuration = SwipeDetector.calculateDuration(velocity,
-                    mToState == targetState ? (1 - progress) : progress);
-            // snap to top or bottom using the release velocity
-        } else {
-            if (progress > SUCCESS_TRANSITION_PROGRESS) {
-                targetState = mToState;
-                animationDuration = SwipeDetector.calculateDuration(velocity, 1 - progress);
-            } else {
-                targetState = mToState == mTargetState ? mBaseState : mTargetState;
-                animationDuration = SwipeDetector.calculateDuration(velocity, progress);
-            }
-        }
-
-        mCurrentAnimation.setEndAction(() -> {
-            mLauncher.getStateManager().goToState(targetState, false);
-            onTransitionComplete(fling, targetState == mToState);
-            mDetector.finishedScrolling();
-            mCurrentAnimation = null;
-        });
-
-        float nextFrameProgress = Utilities.boundToRange(
-                progress + velocity * SINGLE_FRAME_MS * mProgressMultiplier, 0f, 1f);
-
-        ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
-        anim.setFloatValues(nextFrameProgress, targetState == mToState ? 1f : 0f);
-        anim.setDuration(animationDuration);
-        anim.setInterpolator(scrollInterpolatorForVelocity(velocity));
-        anim.start();
-    }
-
-    protected abstract void onTransitionComplete(boolean wasFling, boolean stateChanged);
-}
diff --git a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
index 01b63be..a11a8c5 100644
--- a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
+++ b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
@@ -20,7 +20,6 @@
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.Color;
-import android.graphics.Region;
 import android.support.v4.graphics.ColorUtils;
 import android.util.AttributeSet;
 import android.widget.TextView;
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsState.java b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsState.java
index 2acd29b..c05d30c 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsState.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsState.java
@@ -63,8 +63,8 @@
     }
 
     @Override
-    public float getHoseatAlpha(Launcher launcher) {
-        return 0;
+    public int getVisibleElements(Launcher launcher) {
+        return ALL_APPS_HEADER | ALL_APPS_CONTENT;
     }
 
     @Override
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
index 76b7e0d..e495477 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
@@ -1,19 +1,3 @@
-/*
- * Copyright (C) 2017 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.launcher3.uioverrides;
 
 import static com.android.launcher3.LauncherState.ALL_APPS;
@@ -21,31 +5,34 @@
 
 import android.view.MotionEvent;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.touch.AbstractStateChangeTouchController;
 import com.android.launcher3.touch.SwipeDetector;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.util.VerticalSwipeController;
 
 /**
- * Extension of {@link VerticalSwipeController} to switch between NORMAL and ALL_APPS state.
+ * TouchController to switch between NORMAL and ALL_APPS state.
  */
-public class AllAppsSwipeController extends VerticalSwipeController {
-
-    private int mStartContainerType;
+public class AllAppsSwipeController extends AbstractStateChangeTouchController {
 
     public AllAppsSwipeController(Launcher l) {
-        super(l, NORMAL);
+        super(l, SwipeDetector.VERTICAL);
     }
 
     @Override
-    protected boolean shouldInterceptTouch(MotionEvent ev) {
+    protected boolean canInterceptTouch(MotionEvent ev) {
+        if (mCurrentAnimation != null) {
+            // If we are already animating from a previous state, we can intercept.
+            return true;
+        }
+        if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
+            return false;
+        }
         if (!mLauncher.isInState(NORMAL) && !mLauncher.isInState(ALL_APPS)) {
             // Don't listen for the swipe gesture if we are already in some other state.
             return false;
         }
-
         if (mLauncher.isInState(ALL_APPS) && !mLauncher.getAppsView().shouldContainerScroll(ev)) {
             return false;
         }
@@ -56,8 +43,12 @@
     protected int getSwipeDirection(MotionEvent ev) {
         if (mLauncher.isInState(ALL_APPS)) {
             mStartContainerType = ContainerType.ALLAPPS;
+            mFromState = ALL_APPS;
+            mToState = NORMAL;
             return SwipeDetector.DIRECTION_NEGATIVE;
         } else {
+            mFromState = NORMAL;
+            mToState = ALL_APPS;
             mStartContainerType = mLauncher.getDragLayer().isEventOverHotseat(ev) ?
                     ContainerType.HOTSEAT : ContainerType.WORKSPACE;
             return SwipeDetector.DIRECTION_POSITIVE;
@@ -65,14 +56,14 @@
     }
 
     @Override
-    protected void onTransitionComplete(boolean wasFling, boolean stateChanged) {
-        if (stateChanged) {
-            // Transition complete. log the action
-            mLauncher.getUserEventDispatcher().logActionOnContainer(
-                    wasFling ? Touch.FLING : Touch.SWIPE,
-                    mLauncher.isInState(ALL_APPS) ? Direction.UP : Direction.DOWN,
-                    mStartContainerType,
-                    mLauncher.getWorkspace().getCurrentPage());
-        }
+    protected float initCurrentAnimation() {
+        float range = getShiftRange();
+        long maxAccuracy = (long) (2 * range);
+        mCurrentAnimation = mLauncher.getStateManager()
+                .createAnimationToNewWorkspace(mToState, maxAccuracy);
+        float startVerticalShift = mFromState.getVerticalProgress(mLauncher) * range;
+        float endVerticalShift = mToState.getVerticalProgress(mLauncher) * range;
+        float totalShift = endVerticalShift - startVerticalShift;
+        return 1 / totalShift;
     }
 }
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/OverviewState.java b/src_ui_overrides/com/android/launcher3/uioverrides/OverviewState.java
index 3dfbc40..8def0d3 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/OverviewState.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/OverviewState.java
@@ -16,16 +16,8 @@
 package com.android.launcher3.uioverrides;
 
 import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
-import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
 
-import android.graphics.Rect;
-import android.view.View;
-import android.view.accessibility.AccessibilityNodeInfo;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.Workspace;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 
 /**