Merge "Moving some notification binder calls to worker thread" into ub-launcher3-master
diff --git a/Android.mk b/Android.mk
index 3d1d996..5def65f 100644
--- a/Android.mk
+++ b/Android.mk
@@ -200,7 +200,7 @@
     $(LOCAL_PATH)/quickstep/recents_ui_overrides/res
 
 LOCAL_FULL_LIBS_MANIFEST_FILES := \
-    $(LOCAL_PATH)/AndroidManifest.xml \
+    $(LOCAL_PATH)/quickstep/AndroidManifest-launcher.xml \
     $(LOCAL_PATH)/AndroidManifest-common.xml
 
 LOCAL_MANIFEST_FILE := quickstep/AndroidManifest.xml
@@ -247,7 +247,7 @@
 
 LOCAL_FULL_LIBS_MANIFEST_FILES := \
     $(LOCAL_PATH)/go/AndroidManifest.xml \
-    $(LOCAL_PATH)/AndroidManifest.xml \
+    $(LOCAL_PATH)/quickstep/AndroidManifest-launcher.xml \
     $(LOCAL_PATH)/AndroidManifest-common.xml
 
 LOCAL_MANIFEST_FILE := quickstep/AndroidManifest.xml
@@ -293,7 +293,7 @@
 
 LOCAL_FULL_LIBS_MANIFEST_FILES := \
     $(LOCAL_PATH)/go/AndroidManifest.xml \
-    $(LOCAL_PATH)/AndroidManifest.xml \
+    $(LOCAL_PATH)/quickstep/AndroidManifest-launcher.xml \
     $(LOCAL_PATH)/AndroidManifest-common.xml
 
 LOCAL_MANIFEST_FILE := quickstep/AndroidManifest.xml
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/go/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
new file mode 100644
index 0000000..0c60468
--- /dev/null
+++ b/go/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -0,0 +1,46 @@
+/**
+ * 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.uioverrides;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
+import com.android.launcher3.uioverrides.touchcontrollers.LandscapeStatesTouchController;
+import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
+import com.android.launcher3.util.TouchController;
+import com.android.quickstep.SysUINavigationMode;
+
+import java.util.ArrayList;
+
+public class QuickstepLauncher extends BaseQuickstepLauncher {
+
+    public static final boolean GO_LOW_RAM_RECENTS_ENABLED = true;
+
+    @Override
+    public TouchController[] createTouchControllers() {
+        ArrayList<TouchController> list = new ArrayList<>();
+        list.add(getDragController());
+
+        if (getDeviceProfile().isVerticalBarLayout()) {
+            list.add(new LandscapeStatesTouchController(this));
+            list.add(new LandscapeEdgeSwipeController(this));
+        } else {
+            boolean allowDragToOverview = SysUINavigationMode.INSTANCE.get(this)
+                    .getMode().hasGestures;
+            list.add(new PortraitStatesTouchController(this, allowDragToOverview));
+        }
+        return list.toArray(new TouchController[list.size()]);
+    }
+}
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/RecentsUiFactory.java b/go/quickstep/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
deleted file mode 100644
index d5ea1ec..0000000
--- a/go/quickstep/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * 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.uioverrides;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherStateManager.StateHandler;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.graphics.RotationMode;
-import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
-import com.android.launcher3.uioverrides.touchcontrollers.LandscapeStatesTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchController;
-import com.android.launcher3.util.TouchController;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.views.IconRecentsView;
-
-import java.util.ArrayList;
-
-/**
- * Provides recents-related {@link UiFactory} logic and classes.
- */
-public abstract class RecentsUiFactory {
-
-    public static final boolean GO_LOW_RAM_RECENTS_ENABLED = true;
-
-    public static TouchController[] createTouchControllers(Launcher launcher) {
-        ArrayList<TouchController> list = new ArrayList<>();
-        list.add(launcher.getDragController());
-
-        if (launcher.getDeviceProfile().isVerticalBarLayout()) {
-            list.add(new LandscapeStatesTouchController(launcher));
-            list.add(new LandscapeEdgeSwipeController(launcher));
-        } else {
-            boolean allowDragToOverview = SysUINavigationMode.INSTANCE.get(launcher)
-                    .getMode().hasGestures;
-            list.add(new PortraitStatesTouchController(launcher, allowDragToOverview));
-        }
-        if (Utilities.IS_DEBUG_DEVICE
-                && !launcher.getDeviceProfile().isMultiWindowMode
-                && !launcher.getDeviceProfile().isVerticalBarLayout()) {
-            list.add(new StatusBarTouchController(launcher));
-        }
-        return list.toArray(new TouchController[list.size()]);
-    }
-
-    /**
-     * Creates and returns the controller responsible for recents view state transitions.
-     *
-     * @param launcher the launcher activity
-     * @return state handler for recents
-     */
-    public static StateHandler createRecentsViewStateController(Launcher launcher) {
-        return new RecentsViewStateController(launcher);
-    }
-
-    /**
-     * Clean-up logic that occurs when recents is no longer in use/visible.
-     *
-     * @param launcher the launcher activity
-     */
-    public static void resetOverview(Launcher launcher) {
-        IconRecentsView recentsView = launcher.getOverviewPanel();
-        recentsView.setTransitionedFromApp(false);
-    }
-
-    /**
-     * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
-     *
-     * @param launcher the launcher activity
-     */
-    public static void onLauncherStateOrResumeChanged(Launcher launcher) {}
-
-    public static RotationMode getRotationMode(DeviceProfile dp) {
-        return RotationMode.NORMAL;
-    }
-}
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
new file mode 100644
index 0000000..60afddb
--- /dev/null
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.launcher3">
+    <uses-sdk android:targetSdkVersion="29" android:minSdkVersion="25"/>
+    <!--
+    Manifest entries specific to Launcher3. This is merged with AndroidManifest-common.xml.
+    Refer comments around specific entries on how to extend individual components.
+    -->
+
+    <application
+        android:backupAgent="com.android.launcher3.LauncherBackupAgent"
+        android:fullBackupOnly="true"
+        android:fullBackupContent="@xml/backupscheme"
+        android:hardwareAccelerated="true"
+        android:icon="@drawable/ic_launcher_home"
+        android:label="@string/derived_app_name"
+        android:theme="@style/AppTheme"
+        android:largeHeap="@bool/config_largeHeap"
+        android:restoreAnyVersion="true"
+        android:supportsRtl="true" >
+
+        <!--
+        Main launcher activity. When extending only change the name, and keep all the
+        attributes and intent filters the same
+        -->
+        <activity
+            android:name="com.android.launcher3.uioverrides.QuickstepLauncher"
+            android:launchMode="singleTask"
+            android:clearTaskOnLaunch="true"
+            android:stateNotNeeded="true"
+            android:windowSoftInputMode="adjustPan"
+            android:screenOrientation="unspecified"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:resizeableActivity="true"
+            android:resumeWhilePausing="true"
+            android:taskAffinity=""
+            android:enabled="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.HOME" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.MONKEY"/>
+                <category android:name="android.intent.category.LAUNCHER_APP" />
+            </intent-filter>
+            <meta-data
+                android:name="com.android.launcher3.grid.control"
+                android:value="${packageName}.grid_control" />
+        </activity>
+
+    </application>
+</manifest>
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
similarity index 76%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
rename to quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 2a22e9d..43cdbdb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -1,4 +1,4 @@
-/*
+/**
  * Copyright (C) 2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,7 +13,6 @@
  * 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;
@@ -21,13 +20,14 @@
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 
 import android.content.Context;
+import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.view.Gravity;
 
+import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.uioverrides.touchcontrollers.FlingAndHoldTouchController;
@@ -49,21 +49,15 @@
 
 import java.util.ArrayList;
 
-/**
- * Provides recents-related {@link UiFactory} logic and classes.
- */
-public abstract class RecentsUiFactory {
-
-    private static final String TAG = RecentsUiFactory.class.getSimpleName();
+public class QuickstepLauncher extends BaseQuickstepLauncher {
 
     public static final boolean GO_LOW_RAM_RECENTS_ENABLED = false;
 
     /**
      * Reusable command for applying the shelf height on the background thread.
      */
-    public static final AsyncCommand SET_SHELF_HEIGHT = (context, arg1, arg2) -> {
-        SystemUiProxy.INSTANCE.get(context).setShelfHeight(arg1 != 0, arg2);
-    };
+    public static final AsyncCommand SET_SHELF_HEIGHT = (context, arg1, arg2) ->
+            SystemUiProxy.INSTANCE.get(context).setShelfHeight(arg1 != 0, arg2);
 
     public static RotationMode ROTATION_LANDSCAPE = new RotationMode(-90) {
         @Override
@@ -138,71 +132,78 @@
         }
     };
 
-    public static RotationMode getRotationMode(DeviceProfile dp) {
+    @Override
+    protected RotationMode getFakeRotationMode(DeviceProfile dp) {
         return !dp.isVerticalBarLayout() ? RotationMode.NORMAL
                 : (dp.isSeascape() ? ROTATION_SEASCAPE : ROTATION_LANDSCAPE);
     }
 
-    public static TouchController[] createTouchControllers(Launcher launcher) {
-        Mode mode = SysUINavigationMode.getMode(launcher);
-
-        ArrayList<TouchController> list = new ArrayList<>();
-        list.add(launcher.getDragController());
-        if (mode == NO_BUTTON) {
-            list.add(new QuickSwitchTouchController(launcher));
-            list.add(new NavBarToHomeTouchController(launcher));
-            list.add(new FlingAndHoldTouchController(launcher));
-        } else {
-            if (launcher.getDeviceProfile().isVerticalBarLayout()) {
-                list.add(new OverviewToAllAppsTouchController(launcher));
-                list.add(new LandscapeEdgeSwipeController(launcher));
-                if (mode.hasGestures) {
-                    list.add(new TransposedQuickSwitchTouchController(launcher));
-                }
-            } else {
-                list.add(new PortraitStatesTouchController(launcher,
-                        mode.hasGestures /* allowDragToOverview */));
-                if (mode.hasGestures) {
-                    list.add(new QuickSwitchTouchController(launcher));
-                }
-            }
-        }
-
-        if (!launcher.getDeviceProfile().isMultiWindowMode) {
-            list.add(new StatusBarTouchController(launcher));
-        }
-
-        list.add(new LauncherTaskViewController(launcher));
-        return list.toArray(new TouchController[list.size()]);
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        onStateOrResumeChanged();
     }
 
-    /**
-     * Creates and returns the controller responsible for recents view state transitions.
-     *
-     * @param launcher the launcher activity
-     * @return state handler for recents
-     */
-    public static StateHandler createRecentsViewStateController(Launcher launcher) {
-        return new RecentsViewStateController(launcher);
+    @Override
+    protected void onActivityFlagsChanged(int changeBits) {
+        super.onActivityFlagsChanged(changeBits);
+
+        if ((changeBits & (ACTIVITY_STATE_DEFERRED_RESUMED | ACTIVITY_STATE_STARTED
+                | ACTIVITY_STATE_USER_ACTIVE | ACTIVITY_STATE_TRANSITION_ACTIVE)) != 0
+                && (getActivityFlags() & ACTIVITY_STATE_TRANSITION_ACTIVE) == 0) {
+            onStateOrResumeChanged();
+        }
     }
 
     /**
      * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
-     *
-     * @param launcher the launcher activity
      */
-    public static void onLauncherStateOrResumeChanged(Launcher launcher) {
-        LauncherState state = launcher.getStateManager().getState();
-        DeviceProfile profile = launcher.getDeviceProfile();
-        boolean visible = (state == NORMAL || state == OVERVIEW) && launcher.isUserActive()
+    private void onStateOrResumeChanged() {
+        LauncherState state = getStateManager().getState();
+        DeviceProfile profile = getDeviceProfile();
+        boolean visible = (state == NORMAL || state == OVERVIEW) && isUserActive()
                 && !profile.isVerticalBarLayout();
-        UiThreadHelper.runAsyncCommand(launcher, SET_SHELF_HEIGHT, visible ? 1 : 0,
+        UiThreadHelper.runAsyncCommand(this, SET_SHELF_HEIGHT, visible ? 1 : 0,
                 profile.hotseatBarSizePx);
         if (state == NORMAL) {
-            launcher.<RecentsView>getOverviewPanel().setSwipeDownShouldLaunchApp(false);
+            ((RecentsView) getOverviewPanel()).setSwipeDownShouldLaunchApp(false);
         }
     }
 
+    @Override
+    public TouchController[] createTouchControllers() {
+        Mode mode = SysUINavigationMode.getMode(this);
+
+        ArrayList<TouchController> list = new ArrayList<>();
+        list.add(getDragController());
+        if (mode == NO_BUTTON) {
+            list.add(new QuickSwitchTouchController(this));
+            list.add(new NavBarToHomeTouchController(this));
+            list.add(new FlingAndHoldTouchController(this));
+        } else {
+            if (getDeviceProfile().isVerticalBarLayout()) {
+                list.add(new OverviewToAllAppsTouchController(this));
+                list.add(new LandscapeEdgeSwipeController(this));
+                if (mode.hasGestures) {
+                    list.add(new TransposedQuickSwitchTouchController(this));
+                }
+            } else {
+                list.add(new PortraitStatesTouchController(this,
+                        mode.hasGestures /* allowDragToOverview */));
+                if (mode.hasGestures) {
+                    list.add(new QuickSwitchTouchController(this));
+                }
+            }
+        }
+
+        if (!getDeviceProfile().isMultiWindowMode) {
+            list.add(new StatusBarTouchController(this));
+        }
+
+        list.add(new LauncherTaskViewController(this));
+        return list.toArray(new TouchController[list.size()]);
+    }
+
     private static final class LauncherTaskViewController extends
             TaskViewTouchController<Launcher> {
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
index ee2e951..626292e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/FlingAndHoldTouchController.java
@@ -35,12 +35,12 @@
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
-import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
@@ -51,6 +51,7 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.quickstep.SystemUiProxy;
+import com.android.launcher3.util.VibratorWrapper;
 import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.views.RecentsView;
 
@@ -106,8 +107,7 @@
                     }
                 });
                 mPeekAnim.start();
-                recentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
-                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+                VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
 
                 mLauncher.getDragLayer().getScrim().animateToSysuiMultiplier(isPaused ? 0 : 1,
                         peekDuration, 0);
@@ -173,7 +173,7 @@
     }
 
     @Override
-    public void onDragEnd(float velocity, boolean fling) {
+    public void onDragEnd(float velocity) {
         if (mMotionPauseDetector.isPaused() && handlingOverviewAnim()) {
             if (mPeekAnim != null) {
                 mPeekAnim.cancel();
@@ -196,7 +196,7 @@
             });
             overviewAnim.start();
         } else {
-            super.onDragEnd(velocity, fling);
+            super.onDragEnd(velocity);
         }
 
         View searchView = mLauncher.getAppsView().getSearchView();
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index d66af1a..738436a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -43,7 +43,7 @@
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.TouchController;
@@ -52,12 +52,13 @@
 /**
  * Handles swiping up on the nav bar to go home from launcher, e.g. overview or all apps.
  */
-public class NavBarToHomeTouchController implements TouchController, SwipeDetector.Listener {
+public class NavBarToHomeTouchController implements TouchController,
+        SingleAxisSwipeDetector.Listener {
 
     private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL_3;
 
     private final Launcher mLauncher;
-    private final SwipeDetector mSwipeDetector;
+    private final SingleAxisSwipeDetector mSwipeDetector;
     private final float mPullbackDistance;
 
     private boolean mNoIntercept;
@@ -67,7 +68,8 @@
 
     public NavBarToHomeTouchController(Launcher launcher) {
         mLauncher = launcher;
-        mSwipeDetector = new SwipeDetector(mLauncher, this, SwipeDetector.VERTICAL);
+        mSwipeDetector = new SingleAxisSwipeDetector(mLauncher, this,
+                SingleAxisSwipeDetector.VERTICAL);
         mPullbackDistance = mLauncher.getResources().getDimension(R.dimen.home_pullback_distance);
     }
 
@@ -79,7 +81,8 @@
             if (mNoIntercept) {
                 return false;
             }
-            mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
+            mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_POSITIVE,
+                    false /* ignoreSlop */);
         }
 
         if (mNoIntercept) {
@@ -173,7 +176,8 @@
     }
 
     @Override
-    public void onDragEnd(float velocity, boolean fling) {
+    public void onDragEnd(float velocity) {
+        boolean fling = mSwipeDetector.isFling(velocity);
         final int logAction = fling ? Touch.FLING : Touch.SWIPE;
         float progress = mCurrentAnimation.getProgressFraction();
         float interpolatedProgress = PULLBACK_INTERPOLATOR.getInterpolation(progress);
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
index 5c3b55d..a4ac1b0 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -42,7 +42,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.quickstep.SysUINavigationMode;
@@ -59,10 +59,10 @@
     private @Nullable TaskView mTaskToLaunch;
 
     public QuickSwitchTouchController(Launcher launcher) {
-        this(launcher, SwipeDetector.HORIZONTAL);
+        this(launcher, SingleAxisSwipeDetector.HORIZONTAL);
     }
 
-    protected QuickSwitchTouchController(Launcher l, SwipeDetector.Direction dir) {
+    protected QuickSwitchTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) {
         super(l, dir);
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index 00e4f58..ad02de1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -19,6 +19,9 @@
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE;
 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 
 import android.animation.Animator;
@@ -32,7 +35,8 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.BaseSwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.FlingBlockCheck;
 import com.android.launcher3.util.PendingAnimation;
@@ -46,15 +50,14 @@
  * Touch controller for handling task view card swipes
  */
 public abstract class TaskViewTouchController<T extends BaseDraggingActivity>
-        extends AnimatorListenerAdapter implements TouchController, SwipeDetector.Listener {
-
-    private static final String TAG = "OverviewSwipeController";
+        extends AnimatorListenerAdapter implements TouchController,
+        SingleAxisSwipeDetector.Listener {
 
     // 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 T mActivity;
-    private final SwipeDetector mDetector;
+    private final SingleAxisSwipeDetector mDetector;
     private final RecentsView mRecentsView;
     private final int[] mTempCords = new int[2];
 
@@ -74,7 +77,7 @@
     public TaskViewTouchController(T activity) {
         mActivity = activity;
         mRecentsView = activity.getOverviewPanel();
-        mDetector = new SwipeDetector(activity, this, SwipeDetector.VERTICAL);
+        mDetector = new SingleAxisSwipeDetector(activity, this, SingleAxisSwipeDetector.VERTICAL);
     }
 
     private boolean canInterceptTouch() {
@@ -113,7 +116,7 @@
             int directionsToDetectScroll = 0;
             boolean ignoreSlopWhenSettling = false;
             if (mCurrentAnimation != null) {
-                directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
+                directionsToDetectScroll = DIRECTION_BOTH;
                 ignoreSlopWhenSettling = true;
             } else {
                 mTaskBeingDragged = null;
@@ -126,12 +129,12 @@
                         if (!SysUINavigationMode.getMode(mActivity).hasGestures) {
                             // Don't allow swipe down to open if we don't support swipe up
                             // to enter overview.
-                            directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
+                            directionsToDetectScroll = DIRECTION_POSITIVE;
                         } else {
                             // The task can be dragged up to dismiss it,
                             // and down to open if it's the current page.
                             directionsToDetectScroll = i == mRecentsView.getCurrentPage()
-                                    ? SwipeDetector.DIRECTION_BOTH : SwipeDetector.DIRECTION_POSITIVE;
+                                    ? DIRECTION_BOTH : DIRECTION_POSITIVE;
                         }
                         break;
                     }
@@ -165,8 +168,8 @@
             return;
         }
         int scrollDirections = mDetector.getScrollDirections();
-        if (goingUp && ((scrollDirections & SwipeDetector.DIRECTION_POSITIVE) == 0)
-                || !goingUp && ((scrollDirections & SwipeDetector.DIRECTION_NEGATIVE) == 0)) {
+        if (goingUp && ((scrollDirections & DIRECTION_POSITIVE) == 0)
+                || !goingUp && ((scrollDirections & DIRECTION_NEGATIVE) == 0)) {
             // Trying to re-init in an unsupported direction.
             return;
         }
@@ -243,7 +246,8 @@
     }
 
     @Override
-    public void onDragEnd(float velocity, boolean fling) {
+    public void onDragEnd(float velocity) {
+        boolean fling = mDetector.isFling(velocity);
         final boolean goingToEnd;
         final int logAction;
         boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
@@ -260,7 +264,7 @@
             logAction = Touch.SWIPE;
             goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS;
         }
-        long animationDuration = SwipeDetector.calculateDuration(
+        long animationDuration = BaseSwipeDetector.calculateDuration(
                 velocity, goingToEnd ? (1 - progress) : progress);
         if (blockedFling && !goingToEnd) {
             animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity);
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
index f1e4041..0ed5291 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TransposedQuickSwitchTouchController.java
@@ -17,12 +17,12 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 
 public class TransposedQuickSwitchTouchController extends QuickSwitchTouchController {
 
     public TransposedQuickSwitchTouchController(Launcher launcher) {
-        super(launcher, SwipeDetector.VERTICAL);
+        super(launcher, SingleAxisSwipeDetector.VERTICAL);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
index 939656e..1d00825 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -15,15 +15,12 @@
  */
 package com.android.quickstep;
 
-import static android.os.VibrationEffect.EFFECT_CLICK;
-import static android.os.VibrationEffect.createPredefined;
-
 import static com.android.launcher3.Utilities.postAsyncCallback;
 import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 
 import android.animation.Animator;
@@ -38,9 +35,6 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
-import android.provider.Settings;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.animation.Interpolator;
@@ -55,6 +49,7 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.quickstep.BaseActivityInterface.HomeAnimationFactory;
 import com.android.quickstep.SysUINavigationMode.Mode;
@@ -96,6 +91,7 @@
     protected float mDragLengthFactor = 1;
 
     protected final Context mContext;
+    protected final RecentsAnimationDeviceState mDeviceState;
     protected final GestureState mGestureState;
     protected final OverviewComponentObserver mOverviewComponentObserver;
     protected final BaseActivityInterface<T> mActivityInterface;
@@ -105,9 +101,6 @@
     protected final AppWindowAnimationHelper mAppWindowAnimationHelper;
     protected final TransformParams mTransformParams = new TransformParams();
 
-    private final Vibrator mVibrator;
-    protected final Mode mMode;
-
     // Shift in the range of [0, 1].
     // 0 => preview snapShot is completely visible, and hotseat is completely translated down
     // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
@@ -135,10 +128,11 @@
     protected boolean mCanceled;
     protected int mFinishingRecentsAnimationForNewTaskId = -1;
 
-    protected BaseSwipeUpHandler(Context context, GestureState gestureState,
-            OverviewComponentObserver overviewComponentObserver,
+    protected BaseSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
+            GestureState gestureState, OverviewComponentObserver overviewComponentObserver,
             RecentsModel recentsModel, InputConsumerController inputConsumer, int runningTaskId) {
         mContext = context;
+        mDeviceState = deviceState;
         mGestureState = gestureState;
         mOverviewComponentObserver = overviewComponentObserver;
         mActivityInterface = gestureState.getActivityInterface();
@@ -147,29 +141,15 @@
                 mActivityInterface.createActivityInitListener(this::onActivityInit);
         mRunningTaskId = runningTaskId;
         mInputConsumer = inputConsumer;
-        mMode = SysUINavigationMode.getMode(context);
 
         mAppWindowAnimationHelper = new AppWindowAnimationHelper(context);
         mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
-        mVibrator = context.getSystemService(Vibrator.class);
         initTransitionEndpoints(InvariantDeviceProfile.INSTANCE.get(mContext)
                 .getDeviceProfile(mContext));
     }
 
     protected void performHapticFeedback() {
-        if (!mVibrator.hasVibrator()) {
-            return;
-        }
-        if (Settings.System.getInt(
-                mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) == 0) {
-            return;
-        }
-
-        VibrationEffect effect = createPredefined(EFFECT_CLICK);
-        if (effect == null) {
-            return;
-        }
-        UI_HELPER_EXECUTOR.execute(() -> mVibrator.vibrate(effect));
+        VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
     }
 
     public Consumer<MotionEvent> getRecentsViewDispatcher(RotationMode rotationMode) {
@@ -348,7 +328,7 @@
             mAppWindowAnimationHelper.updateHomeBounds(getStackBounds(dp));
         }
         mAppWindowAnimationHelper.updateTargetRect(TEMP_RECT);
-        if (mMode == Mode.NO_BUTTON) {
+        if (mDeviceState.isFullyGesturalNavMode()) {
             // We can drag all the way to the top of the screen.
             mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
         }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
index 3d1ecef..f889bc1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
@@ -75,6 +75,9 @@
     @Override
     public void onSwipeUpToRecentsComplete() {
         RecentsActivity activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
         RecentsView recentsView = activity.getOverviewPanel();
         recentsView.getClearAllButton().setVisibilityAlpha(1);
         recentsView.setDisallowScrollToClearAll(false);
@@ -236,12 +239,18 @@
     public void onLaunchTaskFailed() {
         // TODO: probably go back to overview instead.
         RecentsActivity activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
         activity.<RecentsView>getOverviewPanel().startHome();
     }
 
     @Override
     public void onLaunchTaskSuccess() {
         RecentsActivity activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
         activity.onTaskLaunched();
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
index 4406314..48b8fc6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -94,6 +94,9 @@
     @Override
     public void onTransitionCancelled(boolean activityVisible) {
         Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
         LauncherState startState = launcher.getStateManager().getRestState();
         launcher.getStateManager().goToState(startState, activityVisible);
     }
@@ -102,32 +105,40 @@
     public void onSwipeUpToRecentsComplete() {
         // Re apply state in case we did something funky during the transition.
         Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
         launcher.getStateManager().reapplyState();
         DiscoveryBounce.showForOverviewIfNeeded(launcher);
     }
 
     @Override
     public void onSwipeUpToHomeComplete() {
+        Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
         // Ensure recents is at the correct position for NORMAL state. For example, when we detach
         // recents, we assume the first task is invisible, making translation off by one task.
-        Launcher launcher = getCreatedActivity();
         launcher.getStateManager().reapplyState();
         setLauncherHideBackArrow(false);
     }
 
     private void setLauncherHideBackArrow(boolean hideBackArrow) {
         Launcher launcher = getCreatedActivity();
-        if (launcher != null) {
-            launcher.getRootView().setForceHideBackArrow(hideBackArrow);
+        if (launcher == null) {
+            return;
         }
+        launcher.getRootView().setForceHideBackArrow(hideBackArrow);
     }
 
     @Override
     public void onAssistantVisibilityChanged(float visibility) {
         Launcher launcher = getCreatedActivity();
-        if (launcher != null) {
-            launcher.onAssistantVisibilityChanged(visibility);
+        if (launcher == null) {
+            return;
         }
+        launcher.onAssistantVisibilityChanged(visibility);
     }
 
     @NonNull
@@ -476,12 +487,18 @@
     @Override
     public void onLaunchTaskFailed() {
         Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
         launcher.getStateManager().goToState(OVERVIEW);
     }
 
     @Override
     public void onLaunchTaskSuccess() {
         Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
         launcher.getStateManager().moveToRestState();
     }
 
@@ -503,6 +520,9 @@
     public void switchRunningTaskViewToScreenshot(ThumbnailData thumbnailData,
             Runnable onFinishRunnable) {
         Launcher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return;
+        }
         RecentsView recentsView = launcher.getOverviewPanel();
         if (recentsView == null) {
             if (onFinishRunnable != null) {
@@ -516,8 +536,9 @@
     @Override
     public void setOnDeferredActivityLaunchCallback(Runnable r) {
         Launcher launcher = getCreatedActivity();
-        if (launcher != null) {
-            launcher.setOnDeferredActivityLaunchCallback(r);
+        if (launcher == null) {
+            return;
         }
+        launcher.setOnDeferredActivityLaunchCallback(r);
     }
 }
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
index ce533a6..19c289d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -112,11 +112,6 @@
 
     @Override
     protected boolean isLauncherInitialized() {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE,
-                    "isLauncherInitialized.TouchInteractionService.isInitialized=" +
-                            TouchInteractionService.isInitialized());
-        }
         return super.isLauncherInitialized() && TouchInteractionService.isInitialized();
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
index 17457aa..b5441df 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
@@ -19,11 +19,11 @@
 import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
 
 import android.graphics.Matrix;
-import android.view.View;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
+import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.ResourceBasedOverride;
 import com.android.quickstep.views.TaskThumbnailView;
@@ -40,30 +40,30 @@
 public class TaskOverlayFactory implements ResourceBasedOverride {
 
     /** Note that these will be shown in order from top to bottom, if available for the task. */
-    private static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[]{
-            new TaskSystemShortcut.AppInfo(),
-            new TaskSystemShortcut.SplitScreen(),
-            new TaskSystemShortcut.Pin(),
-            new TaskSystemShortcut.Install(),
-            new TaskSystemShortcut.Freeform()
+    private static final TaskShortcutFactory[] MENU_OPTIONS = new TaskShortcutFactory[]{
+            TaskShortcutFactory.APP_INFO,
+            TaskShortcutFactory.SPLIT_SCREEN,
+            TaskShortcutFactory.PIN,
+            TaskShortcutFactory.INSTALL,
+            TaskShortcutFactory.FREE_FORM,
+            TaskShortcutFactory.WELLBEING
     };
 
-    public static final MainThreadInitializedObject<TaskOverlayFactory> INSTANCE =
-            forOverride(TaskOverlayFactory.class, R.string.task_overlay_factory_class);
-
-    public List<TaskSystemShortcut> getEnabledShortcuts(TaskView taskView) {
-        final ArrayList<TaskSystemShortcut> shortcuts = new ArrayList<>();
+    public static List<SystemShortcut> getEnabledShortcuts(TaskView taskView) {
+        final ArrayList<SystemShortcut> shortcuts = new ArrayList<>();
         final BaseDraggingActivity activity = BaseActivity.fromContext(taskView.getContext());
-        for (TaskSystemShortcut menuOption : MENU_OPTIONS) {
-            View.OnClickListener onClickListener =
-                    menuOption.getOnClickListener(activity, taskView);
-            if (onClickListener != null) {
-                shortcuts.add(menuOption);
+        for (TaskShortcutFactory menuOption : MENU_OPTIONS) {
+            SystemShortcut shortcut = menuOption.getShortcut(activity, taskView);
+            if (shortcut != null) {
+                shortcuts.add(shortcut);
             }
         }
         return shortcuts;
     }
 
+    public static final MainThreadInitializedObject<TaskOverlayFactory> INSTANCE =
+            forOverride(TaskOverlayFactory.class, R.string.task_overlay_factory_class);
+
     public TaskOverlay createOverlay(TaskThumbnailView thumbnailView) {
         return new TaskOverlay();
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java
new file mode 100644
index 0000000..9ba2e5a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java
@@ -0,0 +1,315 @@
+/*
+ * 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 android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
+
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.view.View;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.model.WellbeingModel;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.popup.SystemShortcut.AppInfo;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.InstantAppResolver;
+import com.android.quickstep.views.RecentsView;
+import com.android.quickstep.views.TaskThumbnailView;
+import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
+import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
+import com.android.systemui.shared.recents.view.RecentsTransition;
+import com.android.systemui.shared.system.ActivityCompat;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
+import com.android.systemui.shared.system.WindowManagerWrapper;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Represents a system shortcut that can be shown for a recent task.
+ */
+public interface TaskShortcutFactory {
+
+    SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView view);
+
+    static WorkspaceItemInfo dummyInfo(TaskView view) {
+        Task task = view.getTask();
+
+        WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo();
+        dummyInfo.intent = new Intent();
+        ComponentName component = task.getTopComponent();
+        dummyInfo.intent.setComponent(component);
+        dummyInfo.user = UserHandle.of(task.key.userId);
+        dummyInfo.title = TaskUtils.getTitle(view.getContext(), task);
+        return dummyInfo;
+    }
+
+    TaskShortcutFactory APP_INFO = (activity, view) -> new AppInfo(activity, dummyInfo(view));
+
+    abstract class MultiWindowFactory implements TaskShortcutFactory {
+
+        private final int mIconRes;
+        private final int mTextRes;
+
+        MultiWindowFactory(int iconRes, int textRes) {
+            mIconRes = iconRes;
+            mTextRes = textRes;
+        }
+
+        protected abstract boolean isAvailable(BaseDraggingActivity activity, int displayId);
+        protected abstract ActivityOptions makeLaunchOptions(Activity activity);
+        protected abstract boolean onActivityStarted(BaseDraggingActivity activity);
+
+        @Override
+        public SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView taskView) {
+            final Task task  = taskView.getTask();
+            if (!task.isDockable) {
+                return null;
+            }
+            if (!isAvailable(activity, task.key.displayId)) {
+                return null;
+            }
+            return new MultiWindowSystemShortcut(mIconRes, mTextRes, activity, taskView, this);
+        }
+    }
+
+    class MultiWindowSystemShortcut extends SystemShortcut {
+
+        private Handler mHandler;
+
+        private final RecentsView mRecentsView;
+        private final TaskThumbnailView mThumbnailView;
+        private final TaskView mTaskView;
+        private final MultiWindowFactory mFactory;
+
+        public MultiWindowSystemShortcut(int iconRes, int textRes,
+                BaseDraggingActivity activity, TaskView taskView, MultiWindowFactory factory) {
+            super(iconRes, textRes, activity, dummyInfo(taskView));
+
+            mHandler = new Handler(Looper.getMainLooper());
+            mTaskView = taskView;
+            mRecentsView = activity.getOverviewPanel();
+            mThumbnailView = taskView.getThumbnail();
+            mFactory = factory;
+        }
+
+        @Override
+        public void onClick(View view) {
+            Task.TaskKey taskKey = mTaskView.getTask().key;
+            final int taskId = taskKey.id;
+
+            final View.OnLayoutChangeListener onLayoutChangeListener =
+                    new View.OnLayoutChangeListener() {
+                        @Override
+                        public void onLayoutChange(View v, int l, int t, int r, int b,
+                                int oldL, int oldT, int oldR, int oldB) {
+                            mTaskView.getRootView().removeOnLayoutChangeListener(this);
+                            mRecentsView.clearIgnoreResetTask(taskId);
+
+                            // Start animating in the side pages once launcher has been resized
+                            mRecentsView.dismissTask(mTaskView, false, false);
+                        }
+                    };
+
+            final DeviceProfile.OnDeviceProfileChangeListener onDeviceProfileChangeListener =
+                    new DeviceProfile.OnDeviceProfileChangeListener() {
+                        @Override
+                        public void onDeviceProfileChanged(DeviceProfile dp) {
+                            mTarget.removeOnDeviceProfileChangeListener(this);
+                            if (dp.isMultiWindowMode) {
+                                mTaskView.getRootView().addOnLayoutChangeListener(
+                                        onLayoutChangeListener);
+                            }
+                        }
+                    };
+
+            dismissTaskMenuView(mTarget);
+
+            ActivityOptions options = mFactory.makeLaunchOptions(mTarget);
+            if (options != null
+                    && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
+                            options)) {
+                if (!mFactory.onActivityStarted(mTarget)) {
+                    return;
+                }
+                // Add a device profile change listener to kick off animating the side tasks
+                // once we enter multiwindow mode and relayout
+                mTarget.addOnDeviceProfileChangeListener(onDeviceProfileChangeListener);
+
+                final Runnable animStartedListener = () -> {
+                    // Hide the task view and wait for the window to be resized
+                    // TODO: Consider animating in launcher and do an in-place start activity
+                    //       afterwards
+                    mRecentsView.setIgnoreResetTask(taskId);
+                    mTaskView.setAlpha(0f);
+                };
+
+                final int[] position = new int[2];
+                mThumbnailView.getLocationOnScreen(position);
+                final int width = (int) (mThumbnailView.getWidth() * mTaskView.getScaleX());
+                final int height = (int) (mThumbnailView.getHeight() * mTaskView.getScaleY());
+                final Rect taskBounds = new Rect(position[0], position[1],
+                        position[0] + width, position[1] + height);
+
+                // Take the thumbnail of the task without a scrim and apply it back after
+                float alpha = mThumbnailView.getDimAlpha();
+                mThumbnailView.setDimAlpha(0);
+                Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
+                        taskBounds.width(), taskBounds.height(), mThumbnailView, 1f,
+                        Color.BLACK);
+                mThumbnailView.setDimAlpha(alpha);
+
+                AppTransitionAnimationSpecsFuture future =
+                        new AppTransitionAnimationSpecsFuture(mHandler) {
+                    @Override
+                    public List<AppTransitionAnimationSpecCompat> composeSpecs() {
+                        return Collections.singletonList(new AppTransitionAnimationSpecCompat(
+                                taskId, thumbnail, taskBounds));
+                    }
+                };
+                WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture(
+                        future, animStartedListener, mHandler, true /* scaleUp */,
+                        taskKey.displayId);
+            }
+        }
+    }
+
+    TaskShortcutFactory SPLIT_SCREEN = new MultiWindowFactory(
+            R.drawable.ic_split_screen, R.string.recent_task_option_split_screen) {
+
+        @Override
+        protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
+            // Don't show menu-item if already in multi-window and the task is from
+            // the secondary display.
+            // TODO(b/118266305): Temporarily disable splitscreen for secondary display while new
+            // implementation is enabled
+            return !activity.getDeviceProfile().isMultiWindowMode
+                    && (displayId == -1 || displayId == DEFAULT_DISPLAY);
+        }
+
+        @Override
+        protected ActivityOptions makeLaunchOptions(Activity activity) {
+            final ActivityCompat act = new ActivityCompat(activity);
+            final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition(
+                    act.getDisplayId());
+            if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) {
+                return null;
+            }
+            boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT;
+            return ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft);
+        }
+
+        @Override
+        protected boolean onActivityStarted(BaseDraggingActivity activity) {
+            SystemUiProxy.INSTANCE.get(activity).onSplitScreenInvoked();
+            activity.getUserEventDispatcher().logActionOnControl(TAP,
+                    LauncherLogProto.ControlType.SPLIT_SCREEN_TARGET);
+            return true;
+        }
+    };
+
+    TaskShortcutFactory FREE_FORM = new MultiWindowFactory(
+            R.drawable.ic_split_screen, R.string.recent_task_option_freeform) {
+
+        @Override
+        protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
+            return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity);
+        }
+
+        @Override
+        protected ActivityOptions makeLaunchOptions(Activity activity) {
+            ActivityOptions activityOptions = ActivityOptionsCompat.makeFreeformOptions();
+            // Arbitrary bounds only because freeform is in dev mode right now
+            Rect r = new Rect(50, 50, 200, 200);
+            activityOptions.setLaunchBounds(r);
+            return activityOptions;
+        }
+
+        @Override
+        protected boolean onActivityStarted(BaseDraggingActivity activity) {
+            activity.returnToHomescreen();
+            return true;
+        }
+    };
+
+    TaskShortcutFactory PIN = (activity, tv) -> {
+        if (!SystemUiProxy.INSTANCE.get(activity).isActive()) {
+            return null;
+        }
+        if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) {
+            return null;
+        }
+        if (ActivityManagerWrapper.getInstance().isLockToAppActive()) {
+            // We shouldn't be able to pin while an app is locked.
+            return null;
+        }
+        return new PinSystemShortcut(activity, tv);
+    };
+
+    class PinSystemShortcut extends SystemShortcut {
+
+        private static final String TAG = "PinSystemShortcut";
+
+        private final TaskView mTaskView;
+
+        public PinSystemShortcut(BaseDraggingActivity target, TaskView tv) {
+            super(R.drawable.ic_pin, R.string.recent_task_option_pin, target, dummyInfo(tv));
+            mTaskView = tv;
+        }
+
+        @Override
+        public void onClick(View view) {
+            Consumer<Boolean> resultCallback = success -> {
+                if (success) {
+                    SystemUiProxy.INSTANCE.get(mTarget).startScreenPinning(
+                            mTaskView.getTask().key.id);
+                } else {
+                    mTaskView.notifyTaskLaunchFailed(TAG);
+                }
+            };
+            mTaskView.launchTask(true, resultCallback, Executors.MAIN_EXECUTOR.getHandler());
+            dismissTaskMenuView(mTarget);
+        }
+    }
+
+    TaskShortcutFactory INSTALL = (activity, view) ->
+            InstantAppResolver.newInstance(activity).isInstantApp(activity,
+                 view.getTask().getTopComponent().getPackageName())
+                    ? new SystemShortcut.Install(activity, dummyInfo(view)) : null;
+
+    TaskShortcutFactory WELLBEING = (activity, view) ->
+            WellbeingModel.SHORTCUT_FACTORY.getShortcut(activity, dummyInfo(view));
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
deleted file mode 100644
index 5a2e3ff..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
+++ /dev/null
@@ -1,327 +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.quickstep;
-
-import static android.view.Display.DEFAULT_DISPLAY;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
-
-import android.app.Activity;
-import android.app.ActivityOptions;
-import android.content.ComponentName;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.view.View;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.R;
-import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.util.InstantAppResolver;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskThumbnailView;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
-import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
-import com.android.systemui.shared.recents.view.RecentsTransition;
-import com.android.systemui.shared.system.ActivityCompat;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.ActivityOptionsCompat;
-import com.android.systemui.shared.system.WindowManagerWrapper;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.function.Consumer;
-
-/**
- * Represents a system shortcut that can be shown for a recent task.
- */
-public class TaskSystemShortcut<T extends SystemShortcut> extends SystemShortcut {
-
-    private static final String TAG = "TaskSystemShortcut";
-
-    protected T mSystemShortcut;
-
-    public TaskSystemShortcut(T systemShortcut) {
-        super(systemShortcut);
-        mSystemShortcut = systemShortcut;
-    }
-
-    protected TaskSystemShortcut(int iconResId, int labelResId) {
-        super(iconResId, labelResId);
-    }
-
-    @Override
-    public View.OnClickListener getOnClickListener(
-            BaseDraggingActivity activity, ItemInfo itemInfo) {
-        return null;
-    }
-
-    public View.OnClickListener getOnClickListener(BaseDraggingActivity activity, TaskView view) {
-        Task task = view.getTask();
-
-        WorkspaceItemInfo dummyInfo = new WorkspaceItemInfo();
-        dummyInfo.intent = new Intent();
-        ComponentName component = task.getTopComponent();
-        dummyInfo.intent.setComponent(component);
-        dummyInfo.user = UserHandle.of(task.key.userId);
-        dummyInfo.title = TaskUtils.getTitle(activity, task);
-
-        return getOnClickListenerForTask(activity, task, dummyInfo);
-    }
-
-    protected View.OnClickListener getOnClickListenerForTask(
-            BaseDraggingActivity activity, Task task, ItemInfo dummyInfo) {
-        return mSystemShortcut.getOnClickListener(activity, dummyInfo);
-    }
-
-    public static class AppInfo extends TaskSystemShortcut<SystemShortcut.AppInfo> {
-        public AppInfo() {
-            super(new SystemShortcut.AppInfo());
-        }
-    }
-
-    public static abstract class MultiWindow extends TaskSystemShortcut {
-
-        private Handler mHandler;
-
-        public MultiWindow(int iconRes, int textRes) {
-            super(iconRes, textRes);
-            mHandler = new Handler(Looper.getMainLooper());
-        }
-
-        protected abstract boolean isAvailable(BaseDraggingActivity activity, int displayId);
-        protected abstract ActivityOptions makeLaunchOptions(Activity activity);
-        protected abstract boolean onActivityStarted(BaseDraggingActivity activity);
-
-        @Override
-        public View.OnClickListener getOnClickListener(
-                BaseDraggingActivity activity, TaskView taskView) {
-            final Task task  = taskView.getTask();
-            final int taskId = task.key.id;
-            final int displayId = task.key.displayId;
-            if (!task.isDockable) {
-                return null;
-            }
-            if (!isAvailable(activity, displayId)) {
-                return null;
-            }
-            final RecentsView recentsView = activity.getOverviewPanel();
-
-            final TaskThumbnailView thumbnailView = taskView.getThumbnail();
-            return (v -> {
-                final View.OnLayoutChangeListener onLayoutChangeListener =
-                        new View.OnLayoutChangeListener() {
-                            @Override
-                            public void onLayoutChange(View v, int l, int t, int r, int b,
-                                    int oldL, int oldT, int oldR, int oldB) {
-                                taskView.getRootView().removeOnLayoutChangeListener(this);
-                                recentsView.clearIgnoreResetTask(taskId);
-
-                                // Start animating in the side pages once launcher has been resized
-                                recentsView.dismissTask(taskView, false, false);
-                            }
-                        };
-
-                final DeviceProfile.OnDeviceProfileChangeListener onDeviceProfileChangeListener =
-                        new DeviceProfile.OnDeviceProfileChangeListener() {
-                            @Override
-                            public void onDeviceProfileChanged(DeviceProfile dp) {
-                                activity.removeOnDeviceProfileChangeListener(this);
-                                if (dp.isMultiWindowMode) {
-                                    taskView.getRootView().addOnLayoutChangeListener(
-                                            onLayoutChangeListener);
-                                }
-                            }
-                        };
-
-                dismissTaskMenuView(activity);
-
-                ActivityOptions options = makeLaunchOptions(activity);
-                if (options != null
-                        && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
-                                options)) {
-                    if (!onActivityStarted(activity)) {
-                        return;
-                    }
-                    // Add a device profile change listener to kick off animating the side tasks
-                    // once we enter multiwindow mode and relayout
-                    activity.addOnDeviceProfileChangeListener(onDeviceProfileChangeListener);
-
-                    final Runnable animStartedListener = () -> {
-                        // Hide the task view and wait for the window to be resized
-                        // TODO: Consider animating in launcher and do an in-place start activity
-                        //       afterwards
-                        recentsView.setIgnoreResetTask(taskId);
-                        taskView.setAlpha(0f);
-                    };
-
-                    final int[] position = new int[2];
-                    thumbnailView.getLocationOnScreen(position);
-                    final int width = (int) (thumbnailView.getWidth() * taskView.getScaleX());
-                    final int height = (int) (thumbnailView.getHeight() * taskView.getScaleY());
-                    final Rect taskBounds = new Rect(position[0], position[1],
-                            position[0] + width, position[1] + height);
-
-                    // Take the thumbnail of the task without a scrim and apply it back after
-                    float alpha = thumbnailView.getDimAlpha();
-                    thumbnailView.setDimAlpha(0);
-                    Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
-                            taskBounds.width(), taskBounds.height(), thumbnailView, 1f,
-                            Color.BLACK);
-                    thumbnailView.setDimAlpha(alpha);
-
-                    AppTransitionAnimationSpecsFuture future =
-                            new AppTransitionAnimationSpecsFuture(mHandler) {
-                        @Override
-                        public List<AppTransitionAnimationSpecCompat> composeSpecs() {
-                            return Collections.singletonList(new AppTransitionAnimationSpecCompat(
-                                    taskId, thumbnail, taskBounds));
-                        }
-                    };
-                    WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture(
-                            future, animStartedListener, mHandler, true /* scaleUp */, displayId);
-                }
-            });
-        }
-    }
-
-    public static class SplitScreen extends MultiWindow {
-        public SplitScreen() {
-            super(R.drawable.ic_split_screen, R.string.recent_task_option_split_screen);
-        }
-
-        @Override
-        protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
-            // Don't show menu-item if already in multi-window and the task is from
-            // the secondary display.
-            // TODO(b/118266305): Temporarily disable splitscreen for secondary display while new
-            // implementation is enabled
-            return !activity.getDeviceProfile().isMultiWindowMode
-                    && (displayId == -1 || displayId == DEFAULT_DISPLAY);
-        }
-
-        @Override
-        protected ActivityOptions makeLaunchOptions(Activity activity) {
-            final ActivityCompat act = new ActivityCompat(activity);
-            final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition(
-                    act.getDisplayId());
-            if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) {
-                return null;
-            }
-            boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT;
-            return ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft);
-        }
-
-        @Override
-        protected boolean onActivityStarted(BaseDraggingActivity activity) {
-            SystemUiProxy.INSTANCE.get(activity).onSplitScreenInvoked();
-            activity.getUserEventDispatcher().logActionOnControl(TAP,
-                    LauncherLogProto.ControlType.SPLIT_SCREEN_TARGET);
-            return true;
-        }
-    }
-
-    public static class Freeform extends MultiWindow {
-        public Freeform() {
-            super(R.drawable.ic_split_screen, R.string.recent_task_option_freeform);
-        }
-
-        @Override
-        protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
-            return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity);
-        }
-
-        @Override
-        protected ActivityOptions makeLaunchOptions(Activity activity) {
-            ActivityOptions activityOptions = ActivityOptionsCompat.makeFreeformOptions();
-            // Arbitrary bounds only because freeform is in dev mode right now
-            Rect r = new Rect(50, 50, 200, 200);
-            activityOptions.setLaunchBounds(r);
-            return activityOptions;
-        }
-
-        @Override
-        protected boolean onActivityStarted(BaseDraggingActivity activity) {
-            activity.returnToHomescreen();
-            return true;
-        }
-    }
-
-    public static class Pin extends TaskSystemShortcut {
-
-        private static final String TAG = Pin.class.getSimpleName();
-
-        private Handler mHandler;
-
-        public Pin() {
-            super(R.drawable.ic_pin, R.string.recent_task_option_pin);
-            mHandler = new Handler(Looper.getMainLooper());
-        }
-
-        @Override
-        public View.OnClickListener getOnClickListener(
-                BaseDraggingActivity activity, TaskView taskView) {
-            if (!SystemUiProxy.INSTANCE.get(activity).isActive()) {
-                return null;
-            }
-            if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) {
-                return null;
-            }
-            if (ActivityManagerWrapper.getInstance().isLockToAppActive()) {
-                // We shouldn't be able to pin while an app is locked.
-                return null;
-            }
-            return view -> {
-                Consumer<Boolean> resultCallback = success -> {
-                    if (success) {
-                        SystemUiProxy.INSTANCE.get(activity).startScreenPinning(
-                                taskView.getTask().key.id);
-                    } else {
-                        taskView.notifyTaskLaunchFailed(TAG);
-                    }
-                };
-                taskView.launchTask(true, resultCallback, mHandler);
-                dismissTaskMenuView(activity);
-            };
-        }
-    }
-
-    public static class Install extends TaskSystemShortcut<SystemShortcut.Install> {
-        public Install() {
-            super(new SystemShortcut.Install());
-        }
-
-        @Override
-        protected View.OnClickListener getOnClickListenerForTask(
-                BaseDraggingActivity activity, Task task, ItemInfo itemInfo) {
-            if (InstantAppResolver.newInstance(activity).isInstantApp(activity,
-                        task.getTopComponent().getPackageName())) {
-                return mSystemShortcut.createOnClickListener(activity, itemInfo);
-            }
-            return null;
-        }
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index e3fcd2f..b2626e5 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -35,6 +35,7 @@
 import android.app.Service;
 import android.app.TaskInfo;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
@@ -49,33 +50,32 @@
 import android.view.MotionEvent;
 
 import androidx.annotation.BinderThread;
+import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.DiscoveryBounce;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.TraceHelper;
-import com.android.quickstep.SysUINavigationMode.Mode;
-import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
 import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
 import com.android.quickstep.inputconsumers.AssistantInputConsumer;
 import com.android.quickstep.inputconsumers.DeviceLockedInputConsumer;
 import com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer;
 import com.android.quickstep.inputconsumers.OtherActivityInputConsumer;
+import com.android.quickstep.inputconsumers.OverscrollInputConsumer;
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
 import com.android.quickstep.inputconsumers.OverviewWithoutFocusInputConsumer;
-import com.android.quickstep.inputconsumers.QuickCaptureInputConsumer;
 import com.android.quickstep.inputconsumers.ResetGestureInputConsumer;
 import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
+import com.android.systemui.plugins.OverscrollPlugin;
+import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -112,8 +112,7 @@
  * Service connected by system-UI for handling touch interaction.
  */
 @TargetApi(Build.VERSION_CODES.Q)
-public class TouchInteractionService extends Service implements
-        NavigationModeChangeListener {
+public class TouchInteractionService extends Service implements PluginListener<OverscrollPlugin> {
 
     private static final String TAG = "TouchInteractionService";
 
@@ -122,6 +121,8 @@
     private static final String HAS_ENABLED_QUICKSTEP_ONCE = "launcher.has_enabled_quickstep_once";
     private static final int MAX_BACK_NOTIFICATION_COUNT = 3;
     private int mBackGestureNotificationCounter = -1;
+    @Nullable
+    private OverscrollPlugin mOverscrollPlugin;
 
     private final IBinder mMyBinder = new IOverviewProxy.Stub() {
 
@@ -134,9 +135,6 @@
                 TouchInteractionService.this.initInputMonitor();
                 preloadOverview(true /* fromInit */);
             });
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE, "TIS initialized");
-            }
             sIsInitialized = true;
         }
 
@@ -265,20 +263,18 @@
 
     private InputMonitorCompat mInputMonitorCompat;
     private InputEventReceiver mInputEventReceiver;
-    private Mode mMode = Mode.THREE_BUTTONS;
 
     @Override
     public void onCreate() {
         super.onCreate();
-        mDeviceState = new RecentsAnimationDeviceState(this);
-        mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
-
         // Initialize anything here that is needed in direct boot mode.
         // Everything else should be initialized in onUserUnlocked() below.
         mMainChoreographer = Choreographer.getInstance();
         mAM = ActivityManagerWrapper.getInstance();
+        mDeviceState = new RecentsAnimationDeviceState(this);
+        mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
+        mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
 
-        onNavigationModeChanged(SysUINavigationMode.INSTANCE.get(this).addModeChangeListener(this));
         sConnected = true;
     }
 
@@ -301,7 +297,7 @@
             Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "initInputMonitor 1");
         }
         disposeEventHandlers();
-        if (!mMode.hasGestures || !SystemUiProxy.INSTANCE.get(this).isActive()) {
+        if (mDeviceState.isButtonNavMode() || !SystemUiProxy.INSTANCE.get(this).isActive()) {
             return;
         }
         if (TestProtocol.sDebugTracing) {
@@ -320,12 +316,10 @@
         mDeviceState.updateGestureTouchRegions();
     }
 
-    @Override
-    public void onNavigationModeChanged(Mode newMode) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "onNavigationModeChanged " + newMode);
-        }
-        mMode = newMode;
+    /**
+     * Called when the navigation mode changes, guaranteed to be after the device state has updated.
+     */
+    private void onNavigationModeChanged(SysUINavigationMode.Mode mode) {
         initInputMonitor();
         resetHomeBounceSeenOnQuickstepEnabledFirstTime();
     }
@@ -348,6 +342,9 @@
         mBackGestureNotificationCounter = Math.max(0, Utilities.getDevicePrefs(this)
                 .getInt(KEY_BACK_NOTIFICATION_COUNT, MAX_BACK_NOTIFICATION_COUNT));
         resetHomeBounceSeenOnQuickstepEnabledFirstTime();
+
+        PluginManagerWrapper.INSTANCE.get(getBaseContext()).addPluginListener(this,
+                OverscrollPlugin.class, false /* allowMultiple */);
     }
 
     private void onDeferredActivityLaunch() {
@@ -362,7 +359,7 @@
     }
 
     private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() {
-        if (!mDeviceState.isUserUnlocked() || !mMode.hasGestures) {
+        if (!mDeviceState.isUserUnlocked() || mDeviceState.isButtonNavMode()) {
             // Skip if not yet unlocked (can't read user shared prefs) or if the current navigation
             // mode doesn't have gestures
             return;
@@ -397,9 +394,8 @@
 
     @Override
     public void onDestroy() {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE, "TIS destroyed");
-        }
+        PluginManagerWrapper.INSTANCE.get(getBaseContext()).removePluginListener(this);
+
         sIsInitialized = false;
         if (mDeviceState.isUserUnlocked()) {
             mInputConsumer.unregisterInputConsumer();
@@ -407,7 +403,6 @@
         }
         disposeEventHandlers();
         mDeviceState.destroy();
-        SysUINavigationMode.INSTANCE.get(this).removeModeChangeListener(this);
         SystemUiProxy.INSTANCE.get(this).setProxy(null);
 
         sConnected = false;
@@ -428,6 +423,9 @@
             Log.e(TAG, "Unknown event " + ev);
             return;
         }
+        if (!mDeviceState.isUserUnlocked()) {
+            return;
+        }
 
         Object traceToken = TraceHelper.INSTANCE.beginFlagsOverride(
                 TraceHelper.FLAG_ALLOW_BINDER_TRACKING);
@@ -443,7 +441,8 @@
 
                 ActiveGestureLog.INSTANCE.addLog("setInputConsumer", mConsumer.getType());
                 mUncheckedConsumer = mConsumer;
-            } else if (mDeviceState.isUserUnlocked() && mMode == Mode.NO_BUTTON
+            } else if (mDeviceState.isUserUnlocked()
+                    && mDeviceState.isFullyGesturalNavMode()
                     && mDeviceState.canTriggerAssistantAction(event)) {
                 // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we should
                 // not interrupt it. QuickSwitch assumes that interruption can only happen if the
@@ -484,15 +483,15 @@
                 || previousGestureState.isRecentsAnimationRunning()
                         ? newBaseConsumer(previousGestureState, newGestureState, event)
                         : mResetGestureInputConsumer;
-        if (mMode == Mode.NO_BUTTON) {
+        if (mDeviceState.isFullyGesturalNavMode()) {
             if (mDeviceState.canTriggerAssistantAction(event)) {
                 base = new AssistantInputConsumer(this, newGestureState, base, mInputMonitorCompat);
             }
 
-            if (FeatureFlags.ENABLE_QUICK_CAPTURE_GESTURE.get()) {
-                // Put the Compose gesture as higher priority than the Assistant or base gestures
-                base = new QuickCaptureInputConsumer(this, newGestureState, base,
-                        mInputMonitorCompat);
+            if (mOverscrollPlugin != null) {
+                // Put the overscroll gesture as higher priority than the Assistant or base gestures
+                base = new OverscrollInputConsumer(this, newGestureState, base, mInputMonitorCompat,
+                        mOverscrollPlugin);
             }
 
             if (mDeviceState.isScreenPinningActive()) {
@@ -531,9 +530,9 @@
                     () -> mAM.getRunningTask(ACTIVITY_TYPE_ASSISTANT));
             if (!ActivityManagerWrapper.isHomeTask(runningTaskInfo)) {
                 final ComponentName homeComponent =
-                    mOverviewComponentObserver.getHomeIntent().getComponent();
+                        mOverviewComponentObserver.getHomeIntent().getComponent();
                 forceOverviewInputConsumer =
-                    runningTaskInfo.baseIntent.getComponent(). equals(homeComponent);
+                        runningTaskInfo.baseIntent.getComponent().equals(homeComponent);
             }
         }
 
@@ -574,7 +573,8 @@
         final boolean shouldDefer;
         final BaseSwipeUpHandler.Factory factory;
 
-        if (mMode == Mode.NO_BUTTON && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
+        if (mDeviceState.isFullyGesturalNavMode()
+                && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
             shouldDefer = previousGestureState.getFinishingRecentsAnimationTaskId() < 0;
             factory = mFallbackNoButtonFactory;
         } else {
@@ -591,7 +591,7 @@
 
     private InputConsumer createDeviceLockedInputConsumer(GestureState gestureState,
             RunningTaskInfo taskInfo) {
-        if (mMode == Mode.NO_BUTTON && taskInfo != null) {
+        if (mDeviceState.isFullyGesturalNavMode() && taskInfo != null) {
             return new DeviceLockedInputConsumer(this, mDeviceState, mTaskAnimationManager,
                     gestureState, mInputMonitorCompat, taskInfo.taskId);
         } else {
@@ -631,7 +631,7 @@
         if (!mDeviceState.isUserUnlocked()) {
             return;
         }
-        if (!mMode.hasGestures && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
+        if (mDeviceState.isButtonNavMode() && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
             // Prevent the overview from being started before the real home on first boot.
             return;
         }
@@ -698,7 +698,6 @@
             // Dump everything
             mDeviceState.dump(pw);
             pw.println("TouchState:");
-            pw.println("  navMode=" + mMode);
             boolean resumed = mOverviewComponentObserver != null
                     && mOverviewComponentObserver.getActivityInterface().isResumed();
             pw.println("  resumed=" + resumed);
@@ -738,9 +737,9 @@
     private BaseSwipeUpHandler createFallbackNoButtonSwipeHandler(GestureState gestureState,
             RunningTaskInfo runningTask, long touchTimeMs, boolean continuingLastGesture,
             boolean isLikelyToStartNewTask) {
-        return new FallbackNoButtonInputConsumer(this, gestureState, mOverviewComponentObserver,
-                runningTask, mRecentsModel, mInputConsumer, isLikelyToStartNewTask,
-                continuingLastGesture);
+        return new FallbackNoButtonInputConsumer(this, mDeviceState, gestureState,
+                mOverviewComponentObserver, runningTask, mRecentsModel, mInputConsumer,
+                isLikelyToStartNewTask, continuingLastGesture);
     }
 
     protected boolean shouldNotifyBackGesture() {
@@ -763,4 +762,14 @@
         UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
                 .startRecentsActivity(intent, null, listener, null, null));
     }
+
+    @Override
+    public void onPluginConnected(OverscrollPlugin overscrollPlugin, Context context) {
+        mOverscrollPlugin = overscrollPlugin;
+    }
+
+    @Override
+    public void onPluginDisconnected(OverscrollPlugin overscrollPlugin) {
+        mOverscrollPlugin = null;
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
index 22ad180..065f3eb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -72,7 +72,6 @@
 import com.android.quickstep.BaseActivityInterface.AnimationFactory.ShelfAnimState;
 import com.android.quickstep.BaseActivityInterface.HomeAnimationFactory;
 import com.android.quickstep.GestureState.GestureEndTarget;
-import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.AppWindowAnimationHelper.TargetAlphaProvider;
@@ -157,9 +156,7 @@
      */
     private static final int LOG_NO_OP_PAGE_INDEX = -1;
 
-    private final RecentsAnimationDeviceState mDeviceState;
     private final TaskAnimationManager mTaskAnimationManager;
-    private final GestureState mGestureState;
 
     // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
     private RunningWindowAnim mRunningWindowAnim;
@@ -198,11 +195,9 @@
             RunningTaskInfo runningTaskInfo, long touchTimeMs,
             OverviewComponentObserver overviewComponentObserver, boolean continuingLastGesture,
             InputConsumerController inputConsumer, RecentsModel recentsModel) {
-        super(context, gestureState, overviewComponentObserver, recentsModel, inputConsumer,
-                runningTaskInfo.id);
-        mDeviceState = deviceState;
+        super(context, deviceState, gestureState, overviewComponentObserver, recentsModel,
+                inputConsumer, runningTaskInfo.id);
         mTaskAnimationManager = taskAnimationManager;
-        mGestureState = gestureState;
         mTouchTimeMs = touchTimeMs;
         mContinuingLastGesture = continuingLastGesture;
         initStateCallbacks();
@@ -444,7 +439,7 @@
      * Note this method has no effect unless the navigation mode is NO_BUTTON.
      */
     private void maybeUpdateRecentsAttachedState(boolean animate) {
-        if (mMode != Mode.NO_BUTTON || mRecentsView == null) {
+        if (!mDeviceState.isFullyGesturalNavMode() || mRecentsView == null) {
             return;
         }
         RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets == null
@@ -546,7 +541,7 @@
         final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
         if (passed != mPassedOverviewThreshold) {
             mPassedOverviewThreshold = passed;
-            if (mMode != Mode.NO_BUTTON) {
+            if (!mDeviceState.isFullyGesturalNavMode()) {
                 performHapticFeedback();
             }
         }
@@ -730,7 +725,7 @@
         if (!isFling) {
             if (isCancel) {
                 endTarget = LAST_TASK;
-            } else if (mMode == Mode.NO_BUTTON) {
+            } else if (mDeviceState.isFullyGesturalNavMode()) {
                 if (mIsShelfPeeking) {
                     endTarget = RECENTS;
                 } else if (goingToNewTask) {
@@ -751,9 +746,9 @@
             boolean willGoToNewTaskOnSwipeUp =
                     goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity);
 
-            if (mMode == Mode.NO_BUTTON && isSwipeUp && !willGoToNewTaskOnSwipeUp) {
+            if (mDeviceState.isFullyGesturalNavMode() && isSwipeUp && !willGoToNewTaskOnSwipeUp) {
                 endTarget = HOME;
-            } else if (mMode == Mode.NO_BUTTON && isSwipeUp && !mIsShelfPeeking) {
+            } else if (mDeviceState.isFullyGesturalNavMode() && isSwipeUp && !mIsShelfPeeking) {
                 // If swiping at a diagonal, base end target on the faster velocity.
                 endTarget = NEW_TASK;
             } else if (isSwipeUp) {
@@ -793,7 +788,7 @@
             float minFlingVelocity = mContext.getResources()
                     .getDimension(R.dimen.quickstep_fling_min_velocity);
             if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) {
-                if (endTarget == RECENTS && mMode != Mode.NO_BUTTON) {
+                if (endTarget == RECENTS && !mDeviceState.isFullyGesturalNavMode()) {
                     Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams(
                             startShift, endShift, endShift, endVelocity / 1000,
                             mTransitionDragLength, mContext);
@@ -839,7 +834,7 @@
                 }
                 duration = Math.max(duration, mRecentsView.getScroller().getDuration());
             }
-            if (mMode == Mode.NO_BUTTON) {
+            if (mDeviceState.isFullyGesturalNavMode()) {
                 setShelfState(ShelfAnimState.OVERVIEW, interpolator, duration);
             }
         } else if (endTarget == NEW_TASK || endTarget == LAST_TASK) {
@@ -1009,7 +1004,10 @@
 
     @Override
     public void onConsumerAboutToBeSwitched() {
-        if (!mGestureState.isRunningAnimationToLauncher()) {
+        if (mActivity != null) {
+            mActivity.setOnStartCallback(null);
+        }
+        if (mGestureState.getEndTarget() != null && !mGestureState.isRunningAnimationToLauncher()) {
             cancelCurrentAnimation();
         } else {
             reset();
@@ -1088,7 +1086,9 @@
         mRecentsView.onGestureAnimationEnd();
 
         // Reset the callback for deferred activity launches
-        mActivityInterface.setOnDeferredActivityLaunchCallback(null);
+        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            mActivityInterface.setOnDeferredActivityLaunchCallback(null);
+        }
         mActivity.getRootView().setOnApplyWindowInsetsListener(null);
         removeLiveTileOverlay();
     }
@@ -1193,8 +1193,7 @@
         }
         mRecentsView.onSwipeUpAnimationSuccess();
 
-        RecentsModel.INSTANCE.get(mContext).onOverviewShown(false, TAG);
-
+        SystemUiProxy.INSTANCE.get(mContext).onOverviewShown(false, TAG);
         doLogGesture(RECENTS);
         reset();
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
index 152b9c9..7b24bd9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
@@ -50,6 +50,7 @@
 import com.android.quickstep.OverviewComponentObserver;
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.fallback.FallbackRecentsView;
 import com.android.quickstep.util.RectFSpringAnim;
@@ -112,13 +113,13 @@
     private final PointF mEndVelocityPxPerMs = new PointF(0, 0.5f);
     private RunningWindowAnim mFinishAnimation;
 
-    public FallbackNoButtonInputConsumer(Context context, GestureState gestureState,
-            OverviewComponentObserver overviewComponentObserver,
+    public FallbackNoButtonInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
+            GestureState gestureState, OverviewComponentObserver overviewComponentObserver,
             RunningTaskInfo runningTaskInfo, RecentsModel recentsModel,
             InputConsumerController inputConsumer,
             boolean isLikelyToStartNewTask, boolean continuingLastGesture) {
-        super(context, gestureState, overviewComponentObserver, recentsModel, inputConsumer,
-                runningTaskInfo.id);
+        super(context, deviceState, gestureState, overviewComponentObserver, recentsModel,
+                inputConsumer, runningTaskInfo.id);
         mLauncherAlpha.value = 1;
 
         mRunningTaskInfo = runningTaskInfo;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index c479250..aeab4b5 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -54,8 +54,6 @@
 import com.android.quickstep.InputConsumer;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationDeviceState;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.CachedEventDispatcher;
@@ -85,7 +83,6 @@
     private final CachedEventDispatcher mRecentsViewDispatcher = new CachedEventDispatcher();
     private final RunningTaskInfo mRunningTask;
     private final InputMonitorCompat mInputMonitorCompat;
-    private final SysUINavigationMode.Mode mMode;
     private final BaseActivityInterface mActivityInterface;
 
     private final BaseSwipeUpHandler.Factory mHandlerFactory;
@@ -137,7 +134,6 @@
         mGestureState = gestureState;
         mMainThreadHandler = new Handler(Looper.getMainLooper());
         mRunningTask = runningTaskInfo;
-        mMode = SysUINavigationMode.getMode(base);
         mHandlerFactory = handlerFactory;
         mActivityInterface = mGestureState.getActivityInterface();
 
@@ -293,7 +289,7 @@
                         mInteractionHandler.updateDisplacement(displacement - mStartDisplacement);
                     }
 
-                    if (mMode == Mode.NO_BUTTON) {
+                    if (mDeviceState.isFullyGesturalNavMode()) {
                         mMotionPauseDetector.setDisallowPause(upDist < mMotionPauseMinDisplacement
                                 || isLikelyToStartNewTask);
                         mMotionPauseDetector.addPosition(displacement, ev.getEventTime());
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/QuickCaptureInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
similarity index 74%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/QuickCaptureInputConsumer.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
index 9826b3a..e3da98b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/QuickCaptureInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
@@ -24,39 +24,28 @@
 
 import static com.android.launcher3.Utilities.squaredHypot;
 
-import android.app.ActivityOptions;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.graphics.PointF;
-import android.os.Bundle;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.R;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
 import com.android.quickstep.views.RecentsView;
+import com.android.systemui.plugins.OverscrollPlugin;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 /**
- * Input consumer for handling events to launch quick capture from launcher
+ * Input consumer for handling events to pass to an {@code OverscrollPlugin}.
+ *
  * @param <T> Draggable activity subclass used by RecentsView
  */
-public class QuickCaptureInputConsumer<T extends BaseDraggingActivity>
-        extends DelegateInputConsumer {
+public class OverscrollInputConsumer<T extends BaseDraggingActivity> extends DelegateInputConsumer {
 
-    private static final String TAG = "QuickCaptureInputConsumer";
-
-    private static final String QUICK_CAPTURE_PACKAGE = "com.google.auxe.compose";
-    private static final String QUICK_CAPTURE_PACKAGE_DEV = "com.google.auxe.compose.debug";
-
-    private static final String EXTRA_DEVICE_STATE = "deviceState";
-    private static final String DEVICE_STATE_LOCKED = "Locked";
-    private static final String DEVICE_STATE_LAUNCHER = "Launcher";
-    private static final String DEVICE_STATE_APP = "App";
-    private static final String DEVICE_STATE_UNKNOWN = "Unknown";
+    private static final String TAG = "OverscrollInputConsumer";
 
     private static final int ANGLE_THRESHOLD = 35; // Degrees
 
@@ -71,14 +60,16 @@
 
     private final Context mContext;
     private final GestureState mGestureState;
+    @Nullable private final OverscrollPlugin mPlugin;
 
     private RecentsView mRecentsView;
 
-    public QuickCaptureInputConsumer(Context context, GestureState gestureState,
-            InputConsumer delegate, InputMonitorCompat inputMonitor) {
+    public OverscrollInputConsumer(Context context, GestureState gestureState,
+            InputConsumer delegate, InputMonitorCompat inputMonitor, OverscrollPlugin plugin) {
         super(delegate, inputMonitor);
         mContext = context;
         mGestureState = gestureState;
+        mPlugin = plugin;
 
         float slop = ViewConfiguration.get(context).getScaledTouchSlop();
         mSquaredSlop = slop * slop;
@@ -89,7 +80,7 @@
 
     @Override
     public int getType() {
-        return TYPE_QUICK_CAPTURE | mDelegate.getType();
+        return TYPE_OVERSCROLL | mDelegate.getType();
     }
 
     private boolean onActivityInit(Boolean alreadyOnHome) {
@@ -149,7 +140,7 @@
                         mPassedSlop = true;
                         mStartDragPos.set(mLastPos.x, mLastPos.y);
 
-                        if (isValidQuickCaptureGesture()) {
+                        if (isOverscrolled()) {
                             setActive(ev);
                         } else {
                             mState = STATE_DELEGATE_ACTIVE;
@@ -161,8 +152,8 @@
             }
             case ACTION_CANCEL:
             case ACTION_UP:
-                if (mState != STATE_DELEGATE_ACTIVE && mPassedSlop) {
-                    startQuickCapture();
+                if (mState != STATE_DELEGATE_ACTIVE && mPassedSlop && mPlugin != null) {
+                    mPlugin.onOverscroll(getDeviceState());
                 }
 
                 mPassedSlop = false;
@@ -175,7 +166,7 @@
         }
     }
 
-    private boolean isValidQuickCaptureGesture() {
+    private boolean isOverscrolled() {
         // Make sure there isn't an app to quick switch to on our right
         boolean atRightMostApp = (mRecentsView == null || mRecentsView.getRunningTaskIndex() <= 0);
 
@@ -187,37 +178,19 @@
         return atRightMostApp && angleInBounds;
     }
 
-    private void startQuickCapture() {
-        // Inspect our delegate's type to figure out where the user invoked Compose
-        String deviceState = DEVICE_STATE_UNKNOWN;
+    private String getDeviceState() {
+        String deviceState = OverscrollPlugin.DEVICE_STATE_UNKNOWN;
         int consumerType = mDelegate.getType();
         if (((consumerType & InputConsumer.TYPE_OVERVIEW) > 0)
                 || ((consumerType & InputConsumer.TYPE_OVERVIEW_WITHOUT_FOCUS)) > 0) {
-            deviceState = DEVICE_STATE_LAUNCHER;
+            deviceState = OverscrollPlugin.DEVICE_STATE_LAUNCHER;
         } else if ((consumerType & InputConsumer.TYPE_OTHER_ACTIVITY) > 0) {
-            deviceState = DEVICE_STATE_APP;
+            deviceState = OverscrollPlugin.DEVICE_STATE_APP;
         } else if (((consumerType & InputConsumer.TYPE_RESET_GESTURE) > 0)
                 || ((consumerType & InputConsumer.TYPE_DEVICE_LOCKED) > 0)) {
-            deviceState = DEVICE_STATE_LOCKED;
+            deviceState = OverscrollPlugin.DEVICE_STATE_LOCKED;
         }
 
-        // Then launch the app
-        PackageManager pm = mContext.getPackageManager();
-
-        Intent qcIntent = pm.getLaunchIntentForPackage(QUICK_CAPTURE_PACKAGE);
-
-        if (qcIntent == null) {
-            // If we couldn't find the regular app, try the dev version
-            qcIntent = pm.getLaunchIntentForPackage(QUICK_CAPTURE_PACKAGE_DEV);
-        }
-
-        if (qcIntent != null) {
-            qcIntent.putExtra(EXTRA_DEVICE_STATE, deviceState);
-
-            Bundle options = ActivityOptions.makeCustomAnimation(mContext, R.anim.slide_in_right,
-                    0).toBundle();
-
-            mContext.startActivity(qcIntent, options);
-        }
+        return deviceState;
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java
index bbb318a..e2524b1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java
@@ -15,8 +15,8 @@
  */
 package com.android.quickstep.util;
 
-import static com.android.launcher3.uioverrides.RecentsUiFactory.ROTATION_LANDSCAPE;
-import static com.android.launcher3.uioverrides.RecentsUiFactory.ROTATION_SEASCAPE;
+import static com.android.launcher3.uioverrides.QuickstepLauncher.ROTATION_LANDSCAPE;
+import static com.android.launcher3.uioverrides.QuickstepLauncher.ROTATION_SEASCAPE;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 
 import android.content.Context;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
index 5a65c15..ca33605 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -378,10 +378,10 @@
     }
 
     @Override
-    public void resetTaskVisuals() {
-        super.resetTaskVisuals();
+    public void setContentAlpha(float alpha) {
+        super.setContentAlpha(alpha);
         if (mRecentsExtraViewContainer != null) {
-            mRecentsExtraViewContainer.setAlpha(mContentAlpha);
+            mRecentsExtraViewContainer.setAlpha(alpha);
         }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
index 07d0796..80022b4 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
@@ -16,7 +16,6 @@
 
 package com.android.quickstep.views;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA;
 
 import android.animation.Animator;
@@ -26,7 +25,6 @@
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.View;
@@ -41,16 +39,13 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
-import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.TaskOverlayFactory;
-import com.android.quickstep.TaskSystemShortcut;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.views.IconView.OnScaleUpdateListener;
 
-import java.util.List;
-
 /**
  * Contains options for a recent task when long-pressing its icon.
  */
@@ -197,22 +192,15 @@
         params.topMargin = (int) -mThumbnailTopMargin;
         mTaskIcon.setLayoutParams(params);
 
-        final BaseDraggingActivity activity = BaseDraggingActivity.fromContext(getContext());
-        final List<TaskSystemShortcut> shortcuts =
-                TaskOverlayFactory.INSTANCE.get(getContext()).getEnabledShortcuts(taskView);
-        final int count = shortcuts.size();
-        for (int i = 0; i < count; ++i) {
-            final TaskSystemShortcut menuOption = shortcuts.get(i);
-            addMenuOption(menuOption, menuOption.getOnClickListener(activity, taskView));
-        }
+        TaskOverlayFactory.getEnabledShortcuts(taskView).forEach(this::addMenuOption);
     }
 
-    private void addMenuOption(TaskSystemShortcut menuOption, OnClickListener onClickListener) {
+    private void addMenuOption(SystemShortcut menuOption) {
         ViewGroup menuOptionView = (ViewGroup) mActivity.getLayoutInflater().inflate(
                 R.layout.task_view_menu_option, this, false);
         menuOption.setIconAndLabelFor(
                 menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text));
-        menuOptionView.setOnClickListener(onClickListener);
+        menuOptionView.setOnClickListener(menuOption);
         mOptionLayout.addView(menuOptionView);
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
index bfb9613..0dfc39b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
@@ -53,6 +53,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.popup.SystemShortcut;
 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;
@@ -61,7 +62,6 @@
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TaskIconCache;
 import com.android.quickstep.TaskOverlayFactory;
-import com.android.quickstep.TaskSystemShortcut;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.util.TaskCornerRadius;
@@ -713,15 +713,8 @@
                         getContext().getText(R.string.accessibility_close_task)));
 
         final Context context = getContext();
-        final List<TaskSystemShortcut> shortcuts =
-                TaskOverlayFactory.INSTANCE.get(getContext()).getEnabledShortcuts(this);
-        final int count = shortcuts.size();
-        for (int i = 0; i < count; ++i) {
-            final TaskSystemShortcut menuOption = shortcuts.get(i);
-            OnClickListener onClickListener = menuOption.getOnClickListener(mActivity, this);
-            if (onClickListener != null) {
-                info.addAction(menuOption.createAccessibilityAction(context));
-            }
+        for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this)) {
+            info.addAction(s.createAccessibilityAction(context));
         }
 
         if (mDigitalWellBeingToast.hasLimit()) {
@@ -752,16 +745,9 @@
             return true;
         }
 
-        final List<TaskSystemShortcut> shortcuts =
-                TaskOverlayFactory.INSTANCE.get(getContext()).getEnabledShortcuts(this);
-        final int count = shortcuts.size();
-        for (int i = 0; i < count; ++i) {
-            final TaskSystemShortcut menuOption = shortcuts.get(i);
-            if (menuOption.hasHandlerForAction(action)) {
-                OnClickListener onClickListener = menuOption.getOnClickListener(mActivity, this);
-                if (onClickListener != null) {
-                    onClickListener.onClick(this);
-                }
+        for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this)) {
+            if (s.hasHandlerForAction(action)) {
+                s.onClick(this);
                 return true;
             }
         }
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 98aaceb..5d9a009 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -33,4 +33,6 @@
     <!-- Assistant Gesture -->
     <integer name="assistant_gesture_min_time_threshold">200</integer>
     <integer name="assistant_gesture_corner_deg_threshold">20</integer>
+
+    <string name="wellbeing_provider_pkg" translatable="false"></string>
 </resources>
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
new file mode 100644
index 0000000..fc9cfcd
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -0,0 +1,273 @@
+/*
+ * 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;
+
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
+import static com.android.launcher3.AbstractFloatingView.TYPE_HIDE_BACK_BUTTON;
+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.allapps.DiscoveryBounce.BOUNCE_MAX_COUNT;
+import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_COUNT;
+import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN;
+import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_COUNT;
+import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_SEEN;
+
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+
+import com.android.launcher3.LauncherState.ScaleAndTranslation;
+import com.android.launcher3.LauncherStateManager.StateHandler;
+import com.android.launcher3.model.WellbeingModel;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.proxy.ProxyActivityStarter;
+import com.android.launcher3.proxy.StartActivityParams;
+import com.android.launcher3.uioverrides.BackButtonAlphaHandler;
+import com.android.launcher3.uioverrides.RecentsViewStateController;
+import com.android.launcher3.util.UiThreadHelper;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
+import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.RemoteFadeOutAnimationListener;
+
+import java.util.stream.Stream;
+
+/**
+ * Extension of Launcher activity to provide quickstep specific functionality
+ */
+public abstract class BaseQuickstepLauncher extends Launcher
+        implements NavigationModeChangeListener {
+
+    /**
+     * Reusable command for applying the back button alpha on the background thread.
+     */
+    public static final UiThreadHelper.AsyncCommand SET_BACK_BUTTON_ALPHA =
+            (context, arg1, arg2) -> SystemUiProxy.INSTANCE.get(context).setBackButtonAlpha(
+                    Float.intBitsToFloat(arg1), arg2 != 0);
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        SysUINavigationMode.Mode mode = SysUINavigationMode.INSTANCE.get(this)
+                .addModeChangeListener(this);
+        getRotationHelper().setRotationHadDifferentUI(mode != Mode.NO_BUTTON);
+
+        if (!getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)) {
+            getStateManager().addStateListener(new LauncherStateManager.StateListener() {
+                @Override
+                public void onStateTransitionStart(LauncherState toState) { }
+
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    boolean swipeUpEnabled = SysUINavigationMode.INSTANCE
+                            .get(BaseQuickstepLauncher.this).getMode().hasGestures;
+                    LauncherState prevState = getStateManager().getLastState();
+
+                    if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled
+                            && finalState == ALL_APPS && prevState == NORMAL) || BOUNCE_MAX_COUNT
+                            <= getSharedPrefs().getInt(HOME_BOUNCE_COUNT, 0))) {
+                        getSharedPrefs().edit().putBoolean(HOME_BOUNCE_SEEN, true).apply();
+                        getStateManager().removeStateListener(this);
+                    }
+                }
+            });
+        }
+
+        if (!getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false)) {
+            getStateManager().addStateListener(new LauncherStateManager.StateListener() {
+                @Override
+                public void onStateTransitionStart(LauncherState toState) { }
+
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    LauncherState prevState = getStateManager().getLastState();
+
+                    if ((finalState == ALL_APPS && prevState == OVERVIEW) || BOUNCE_MAX_COUNT
+                            <= getSharedPrefs().getInt(SHELF_BOUNCE_COUNT, 0)) {
+                        getSharedPrefs().edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply();
+                        getStateManager().removeStateListener(this);
+                    }
+                }
+            });
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        SysUINavigationMode.INSTANCE.get(this).removeModeChangeListener(this);
+        super.onDestroy();
+    }
+
+    @Override
+    public void onNavigationModeChanged(Mode newMode) {
+        getDragLayer().recreateControllers();
+        getRotationHelper().setRotationHadDifferentUI(newMode != Mode.NO_BUTTON);
+    }
+
+    @Override
+    public void onEnterAnimationComplete() {
+        super.onEnterAnimationComplete();
+        // After the transition to home, enable the high-res thumbnail loader if it wasn't enabled
+        // as a part of quickstep, so that high-res thumbnails can load the next time we enter
+        // overview
+        RecentsModel.INSTANCE.get(this).getThumbnailCache()
+                .getHighResLoadingState().setVisible(true);
+    }
+
+    @Override
+    public void onTrimMemory(int level) {
+        super.onTrimMemory(level);
+        RecentsModel.INSTANCE.get(this).onTrimMemory(level);
+    }
+
+    @Override
+    public void startIntentSenderForResult(IntentSender intent, int requestCode,
+            Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, Bundle options) {
+        if (requestCode != -1) {
+            mPendingActivityRequestCode = requestCode;
+            StartActivityParams params = new StartActivityParams(this, requestCode);
+            params.intentSender = intent;
+            params.fillInIntent = fillInIntent;
+            params.flagsMask = flagsMask;
+            params.flagsValues = flagsValues;
+            params.extraFlags = extraFlags;
+            params.options = options;
+            startActivity(ProxyActivityStarter.getLaunchIntent(this, params));
+        } else {
+            super.startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask,
+                    flagsValues, extraFlags, options);
+        }
+    }
+
+    @Override
+    public void startActivityForResult(Intent intent, int requestCode, Bundle options) {
+        if (requestCode != -1) {
+            mPendingActivityRequestCode = -1;
+            StartActivityParams params = new StartActivityParams(this, requestCode);
+            params.intent = intent;
+            params.options = options;
+            startActivity(ProxyActivityStarter.getLaunchIntent(this, params));
+        } else {
+            super.startActivityForResult(intent, requestCode, options);
+        }
+    }
+
+    @Override
+    protected void onDeferredResumed() {
+        if (mPendingActivityRequestCode != -1 && isInState(NORMAL)) {
+            // Remove any active ProxyActivityStarter task and send RESULT_CANCELED to Launcher.
+            onActivityResult(mPendingActivityRequestCode, RESULT_CANCELED, null);
+            // ProxyActivityStarter is started with clear task to reset the task after which it
+            // removes the task itself.
+            startActivity(ProxyActivityStarter.getLaunchIntent(this, null));
+        }
+    }
+
+    @Override
+    protected StateHandler[] createStateHandlers() {
+        return new StateHandler[] {
+                getAllAppsController(),
+                getWorkspace(),
+                new RecentsViewStateController(this),
+                new BackButtonAlphaHandler(this)};
+    }
+
+    @Override
+    protected ScaleAndTranslation getOverviewScaleAndTranslationForNormalState() {
+        if (SysUINavigationMode.getMode(this) == Mode.NO_BUTTON) {
+            float offscreenTranslationX = getDeviceProfile().widthPx
+                    - getOverviewPanel().getPaddingStart();
+            return new ScaleAndTranslation(1f, offscreenTranslationX, 0f);
+        }
+        return super.getOverviewScaleAndTranslationForNormalState();
+    }
+
+    @Override
+    public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) {
+        QuickstepAppTransitionManagerImpl appTransitionManager =
+                (QuickstepAppTransitionManagerImpl) getAppTransitionManager();
+        appTransitionManager.setRemoteAnimationProvider((appTargets, wallpaperTargets) -> {
+
+            // On the first call clear the reference.
+            signal.cancel();
+
+            ValueAnimator fadeAnimation = ValueAnimator.ofFloat(1, 0);
+            fadeAnimation.addUpdateListener(new RemoteFadeOutAnimationListener(appTargets,
+                    wallpaperTargets));
+            AnimatorSet anim = new AnimatorSet();
+            anim.play(fadeAnimation);
+            return anim;
+        }, signal);
+    }
+
+    @Override
+    public void onDragLayerHierarchyChanged() {
+        onLauncherStateOrFocusChanged();
+    }
+
+    @Override
+    protected void onActivityFlagsChanged(int changeBits) {
+        if ((changeBits
+                & (ACTIVITY_STATE_WINDOW_FOCUSED | ACTIVITY_STATE_TRANSITION_ACTIVE)) != 0) {
+            onLauncherStateOrFocusChanged();
+        }
+
+        super.onActivityFlagsChanged(changeBits);
+    }
+
+    /**
+     * Sets the back button visibility based on the current state/window focus.
+     */
+    private void onLauncherStateOrFocusChanged() {
+        Mode mode = SysUINavigationMode.getMode(this);
+        boolean shouldBackButtonBeHidden = mode.hasGestures
+                && getStateManager().getState().hideBackButton
+                && hasWindowFocus()
+                && (getActivityFlags() & ACTIVITY_STATE_TRANSITION_ACTIVE) == 0;
+        if (shouldBackButtonBeHidden) {
+            // Show the back button if there is a floating view visible.
+            shouldBackButtonBeHidden = AbstractFloatingView.getTopOpenViewWithType(this,
+                    TYPE_ALL & ~TYPE_HIDE_BACK_BUTTON) == null;
+        }
+        UiThreadHelper.setBackButtonAlphaAsync(this, SET_BACK_BUTTON_ALPHA,
+                shouldBackButtonBeHidden ? 0f : 1f, true /* animate */);
+        if (getDragLayer() != null) {
+            getRootView().setDisallowBackGesture(shouldBackButtonBeHidden);
+        }
+    }
+
+    @Override
+    public void finishBindingItems(int pageBoundFirst) {
+        super.finishBindingItems(pageBoundFirst);
+        // Instantiate and initialize WellbeingModel now that its loading won't interfere with
+        // populating workspace.
+        // TODO: Find a better place for this
+        WellbeingModel.get(this);
+    }
+
+    @Override
+    public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
+        return Stream.concat(super.getSupportedShortcuts(),
+                Stream.of(WellbeingModel.SHORTCUT_FACTORY));
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
new file mode 100644
index 0000000..852a08e
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
@@ -0,0 +1,342 @@
+/*
+ * 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.model;
+
+import static android.content.ContentResolver.SCHEME_CONTENT;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.createAndStartNewLooper;
+
+import android.annotation.TargetApi;
+import android.app.RemoteAction;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.LauncherApps;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.DeadObjectException;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Process;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.R;
+import com.android.launcher3.popup.RemoteActionShortcut;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.SimpleBroadcastReceiver;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Data model for digital wellbeing status of apps.
+ */
+@TargetApi(Build.VERSION_CODES.Q)
+public final class WellbeingModel {
+    private static final String TAG = "WellbeingModel";
+    private static final int[] RETRY_TIMES_MS = {5000, 15000, 30000};
+    private static final boolean DEBUG = false;
+
+    private static final int MSG_PACKAGE_ADDED = 1;
+    private static final int MSG_PACKAGE_REMOVED = 2;
+    private static final int MSG_FULL_REFRESH = 3;
+
+    // Welbeing contract
+    private static final String METHOD_GET_ACTIONS = "get_actions";
+    private static final String EXTRA_ACTIONS = "actions";
+    private static final String EXTRA_ACTION = "action";
+    private static final String EXTRA_MAX_NUM_ACTIONS_SHOWN = "max_num_actions_shown";
+    private static final String EXTRA_PACKAGES = "packages";
+
+    private static WellbeingModel sInstance;
+
+    private final Context mContext;
+    private final String mWellbeingProviderPkg;
+    private final Handler mWorkerHandler;
+
+    private final ContentObserver mContentObserver;
+
+    private final Object mModelLock = new Object();
+    // Maps the action Id to the corresponding RemoteAction
+    private final Map<String, RemoteAction> mActionIdMap = new ArrayMap<>();
+    private final Map<String, String> mPackageToActionId = new HashMap<>();
+
+    private boolean mIsInTest;
+
+    private WellbeingModel(final Context context) {
+        mContext = context;
+        mWorkerHandler =
+                new Handler(createAndStartNewLooper("WellbeingHandler"), this::handleMessage);
+
+        mWellbeingProviderPkg = mContext.getString(R.string.wellbeing_provider_pkg);
+        mContentObserver = new ContentObserver(MAIN_EXECUTOR.getHandler()) {
+            @Override
+            public void onChange(boolean selfChange, Uri uri) {
+                // Wellbeing reports that app actions have changed.
+                if (DEBUG || mIsInTest) {
+                    Log.d(TAG, "ContentObserver.onChange() called with: selfChange = [" + selfChange
+                            + "], uri = [" + uri + "]");
+                }
+                Preconditions.assertUIThread();
+                updateWellbeingData();
+            }
+        };
+
+        if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
+            context.registerReceiver(
+                    new SimpleBroadcastReceiver(this::onWellbeingProviderChanged),
+                    PackageManagerHelper.getPackageFilter(mWellbeingProviderPkg,
+                            Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED,
+                            Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_DATA_CLEARED,
+                            Intent.ACTION_PACKAGE_RESTARTED));
+
+            IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+            filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+            filter.addDataScheme("package");
+            context.registerReceiver(new SimpleBroadcastReceiver(this::onAppPackageChanged),
+                    filter);
+
+            restartObserver();
+        }
+    }
+
+    public void setInTest(boolean inTest) {
+        mIsInTest = inTest;
+    }
+
+    protected void onWellbeingProviderChanged(Intent intent) {
+        if (DEBUG || mIsInTest) {
+            Log.d(TAG, "Changes to Wellbeing package: intent = [" + intent + "]");
+        }
+        restartObserver();
+    }
+
+    private void restartObserver() {
+        final ContentResolver resolver = mContext.getContentResolver();
+        resolver.unregisterContentObserver(mContentObserver);
+        Uri actionsUri = apiBuilder().path("actions").build();
+        try {
+            resolver.registerContentObserver(
+                    actionsUri, true /* notifyForDescendants */, mContentObserver);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to register content observer for " + actionsUri + ": " + e);
+            if (mIsInTest) throw new RuntimeException(e);
+        }
+        updateWellbeingData();
+    }
+
+    @MainThread
+    public static WellbeingModel get(@NonNull Context context) {
+        Preconditions.assertUIThread();
+        if (sInstance == null) {
+            sInstance = new WellbeingModel(context.getApplicationContext());
+        }
+        return sInstance;
+    }
+
+    @MainThread
+    private SystemShortcut getShortcutForApp(String packageName, int userId,
+            BaseDraggingActivity activity, ItemInfo info) {
+        Preconditions.assertUIThread();
+        // Work profile apps are not recognized by digital wellbeing.
+        if (userId != UserHandle.myUserId()) {
+            if (DEBUG || mIsInTest) {
+                Log.d(TAG, "getShortcutForApp [" + packageName + "]: not current user");
+            }
+            return null;
+        }
+
+        synchronized (mModelLock) {
+            String actionId = mPackageToActionId.get(packageName);
+            final RemoteAction action = actionId != null ? mActionIdMap.get(actionId) : null;
+            if (action == null) {
+                if (DEBUG || mIsInTest) {
+                    Log.d(TAG, "getShortcutForApp [" + packageName + "]: no action");
+                }
+                return null;
+            }
+            if (DEBUG || mIsInTest) {
+                Log.d(TAG,
+                        "getShortcutForApp [" + packageName + "]: action: '" + action.getTitle()
+                                + "'");
+            }
+            return new RemoteActionShortcut(action, activity, info);
+        }
+    }
+
+    private void updateWellbeingData() {
+        mWorkerHandler.sendEmptyMessage(MSG_FULL_REFRESH);
+    }
+
+    private Uri.Builder apiBuilder() {
+        return new Uri.Builder()
+                .scheme(SCHEME_CONTENT)
+                .authority(mWellbeingProviderPkg + ".api");
+    }
+
+    private boolean updateActions(String... packageNames) {
+        if (packageNames.length == 0) {
+            return true;
+        }
+        if (DEBUG || mIsInTest) {
+            Log.d(TAG, "retrieveActions() called with: packageNames = [" + String.join(", ",
+                    packageNames) + "]");
+        }
+        Preconditions.assertNonUiThread();
+
+        Uri contentUri = apiBuilder().build();
+        final Bundle remoteActionBundle;
+        try (ContentProviderClient client = mContext.getContentResolver()
+                .acquireUnstableContentProviderClient(contentUri)) {
+            if (client == null) {
+                if (DEBUG || mIsInTest) Log.i(TAG, "retrieveActions(): null provider");
+                return false;
+            }
+
+            // Prepare wellbeing call parameters.
+            final Bundle params = new Bundle();
+            params.putStringArray(EXTRA_PACKAGES, packageNames);
+            params.putInt(EXTRA_MAX_NUM_ACTIONS_SHOWN, 1);
+            // Perform wellbeing call .
+            remoteActionBundle = client.call(METHOD_GET_ACTIONS, null, params);
+        } catch (DeadObjectException e) {
+            Log.i(TAG, "retrieveActions(): DeadObjectException");
+            return false;
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to retrieve data from " + contentUri + ": " + e);
+            if (mIsInTest) throw new RuntimeException(e);
+            return true;
+        }
+
+        synchronized (mModelLock) {
+            // Remove the entries for requested packages, and then update the fist with what we
+            // got from service
+            Arrays.stream(packageNames).forEach(mPackageToActionId::remove);
+
+            // The result consists of sub-bundles, each one is per a remote action. Each sub-bundle
+            // has a RemoteAction and a list of packages to which the action applies.
+            for (String actionId :
+                    remoteActionBundle.getStringArray(EXTRA_ACTIONS)) {
+                final Bundle actionBundle = remoteActionBundle.getBundle(actionId);
+                mActionIdMap.put(actionId,
+                        actionBundle.getParcelable(EXTRA_ACTION));
+
+                final String[] packagesForAction =
+                        actionBundle.getStringArray(EXTRA_PACKAGES);
+                if (DEBUG || mIsInTest) {
+                    Log.d(TAG, "....actionId: " + actionId + ", packages: " + String.join(", ",
+                            packagesForAction));
+                }
+                for (String packageName : packagesForAction) {
+                    mPackageToActionId.put(packageName, actionId);
+                }
+            }
+        }
+        if (DEBUG || mIsInTest) Log.i(TAG, "retrieveActions(): finished");
+        return true;
+    }
+
+    private boolean handleMessage(Message msg) {
+        switch (msg.what) {
+            case MSG_PACKAGE_REMOVED: {
+                String packageName = (String) msg.obj;
+                mWorkerHandler.removeCallbacksAndMessages(packageName);
+                synchronized (mModelLock) {
+                    mPackageToActionId.remove(packageName);
+                }
+                return true;
+            }
+            case MSG_PACKAGE_ADDED: {
+                String packageName = (String) msg.obj;
+                mWorkerHandler.removeCallbacksAndMessages(packageName);
+                if (!updateActions(packageName)) {
+                    scheduleRefreshRetry(msg);
+                }
+                return true;
+            }
+
+            case MSG_FULL_REFRESH: {
+                // Remove all existing messages
+                mWorkerHandler.removeCallbacksAndMessages(null);
+                final String[] packageNames = mContext.getSystemService(LauncherApps.class)
+                            .getActivityList(null, Process.myUserHandle()).stream()
+                            .map(li -> li.getApplicationInfo().packageName).distinct()
+                            .toArray(String[]::new);
+                if (!updateActions(packageNames)) {
+                    scheduleRefreshRetry(msg);
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void scheduleRefreshRetry(Message originalMsg) {
+        int retryCount = originalMsg.arg1;
+        if (retryCount >= RETRY_TIMES_MS.length) {
+            // To many retries, skip
+            return;
+        }
+
+        Message msg = Message.obtain(originalMsg);
+        msg.arg1 = retryCount + 1;
+        mWorkerHandler.sendMessageDelayed(msg, RETRY_TIMES_MS[retryCount]);
+    }
+
+    private void onAppPackageChanged(Intent intent) {
+        if (DEBUG || mIsInTest) Log.d(TAG, "Changes in apps: intent = [" + intent + "]");
+        Preconditions.assertUIThread();
+
+        final String packageName = intent.getData().getSchemeSpecificPart();
+        if (packageName == null || packageName.length() == 0) {
+            // they sent us a bad intent
+            return;
+        }
+
+        final String action = intent.getAction();
+        if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
+            Message.obtain(mWorkerHandler, MSG_PACKAGE_REMOVED, packageName).sendToTarget();
+        } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
+            Message.obtain(mWorkerHandler, MSG_PACKAGE_ADDED, packageName).sendToTarget();
+        }
+    }
+
+    /**
+     * Shortcut factory for generating wellbeing action
+     */
+    public static final SystemShortcut.Factory SHORTCUT_FACTORY = (activity, info) ->
+            WellbeingModel.get(activity).getShortcutForApp(
+                    info.getTargetComponent().getPackageName(),
+                    info.user.getIdentifier(),
+                    activity, info);
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
new file mode 100644
index 0000000..965b5f0
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -0,0 +1,63 @@
+/*
+ * 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.app.Activity;
+import android.app.Person;
+import android.content.pm.ShortcutInfo;
+import android.util.Base64;
+
+import com.android.launcher3.Utilities;
+import com.android.systemui.shared.system.ActivityCompat;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.util.zip.Deflater;
+
+public class ApiWrapper {
+
+    public static boolean dumpActivity(Activity activity, PrintWriter writer) {
+        if (!Utilities.IS_DEBUG_DEVICE) {
+            return false;
+        }
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        if (!(new ActivityCompat(activity).encodeViewHierarchy(out))) {
+            return false;
+        }
+
+        Deflater deflater = new Deflater();
+        deflater.setInput(out.toByteArray());
+        deflater.finish();
+
+        out.reset();
+        byte[] buffer = new byte[1024];
+        while (!deflater.finished()) {
+            int count = deflater.deflate(buffer); // returns the generated code... index
+            out.write(buffer, 0, count);
+        }
+
+        writer.println("--encoded-view-dump-v0--");
+        writer.println(Base64.encodeToString(
+                out.toByteArray(), Base64.NO_WRAP | Base64.NO_PADDING));
+        return true;
+    }
+
+    public static Person[] getPersons(ShortcutInfo si) {
+        Person[] persons = si.getPersons();
+        return persons == null ? Utilities.EMPTY_PERSON_ARRAY : persons;
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BackButtonAlphaHandler.java b/quickstep/src/com/android/launcher3/uioverrides/BackButtonAlphaHandler.java
index aa0dfc3..43dc882 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BackButtonAlphaHandler.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BackButtonAlphaHandler.java
@@ -16,11 +16,9 @@
 
 package com.android.launcher3.uioverrides;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 
-import com.android.launcher3.Launcher;
+import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager;
 import com.android.launcher3.anim.AnimatorSetBuilder;
@@ -30,18 +28,14 @@
 
 public class BackButtonAlphaHandler implements LauncherStateManager.StateHandler {
 
-    private static final String TAG = "BackButtonAlphaHandler";
+    private final BaseQuickstepLauncher mLauncher;
 
-    private final Launcher mLauncher;
-
-    public BackButtonAlphaHandler(Launcher launcher) {
+    public BackButtonAlphaHandler(BaseQuickstepLauncher launcher) {
         mLauncher = launcher;
     }
 
     @Override
-    public void setState(LauncherState state) {
-        UiFactory.onLauncherStateOrFocusChanged(mLauncher);
-    }
+    public void setState(LauncherState state) { }
 
     @Override
     public void setStateWithAnimation(LauncherState toState,
@@ -52,8 +46,8 @@
 
         if (!SysUINavigationMode.getMode(mLauncher).hasGestures) {
             // If the nav mode is not gestural, then force back button alpha to be 1
-            UiThreadHelper.setBackButtonAlphaAsync(mLauncher, UiFactory.SET_BACK_BUTTON_ALPHA, 1f,
-                    true /* animate */);
+            UiThreadHelper.setBackButtonAlphaAsync(mLauncher,
+                    BaseQuickstepLauncher.SET_BACK_BUTTON_ALPHA, 1f, true /* animate */);
             return;
         }
 
@@ -64,15 +58,8 @@
             anim.setDuration(config.duration);
             anim.addUpdateListener(valueAnimator -> {
                 final float alpha = (float) valueAnimator.getAnimatedValue();
-                UiThreadHelper.setBackButtonAlphaAsync(mLauncher, UiFactory.SET_BACK_BUTTON_ALPHA,
-                        alpha, false /* animate */);
-            });
-            anim.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    // Reapply the final alpha in case some state (e.g. window focus) changed.
-                    UiFactory.onLauncherStateOrFocusChanged(mLauncher);
-                }
+                UiThreadHelper.setBackButtonAlphaAsync(mLauncher,
+                        BaseQuickstepLauncher.SET_BACK_BUTTON_ALPHA, alpha, false /* animate */);
             });
             builder.play(anim);
         }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
deleted file mode 100644
index 17c681b..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ /dev/null
@@ -1,267 +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 android.app.Activity.RESULT_CANCELED;
-
-import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
-import static com.android.launcher3.AbstractFloatingView.TYPE_HIDE_BACK_BUTTON;
-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.allapps.DiscoveryBounce.BOUNCE_MAX_COUNT;
-import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_COUNT;
-import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN;
-import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_COUNT;
-import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_SEEN;
-
-import android.animation.AnimatorSet;
-import android.animation.ValueAnimator;
-import android.app.Activity;
-import android.app.Person;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentSender;
-import android.content.pm.ShortcutInfo;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-import android.util.Base64;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
-import com.android.launcher3.LauncherStateManager;
-import com.android.launcher3.LauncherStateManager.StateHandler;
-import com.android.launcher3.QuickstepAppTransitionManagerImpl;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.proxy.ProxyActivityStarter;
-import com.android.launcher3.proxy.StartActivityParams;
-import com.android.launcher3.util.UiThreadHelper;
-import com.android.quickstep.RecentsModel;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.SysUINavigationMode.Mode;
-import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
-import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.util.RemoteFadeOutAnimationListener;
-import com.android.systemui.shared.system.ActivityCompat;
-
-import java.io.ByteArrayOutputStream;
-import java.io.PrintWriter;
-import java.util.zip.Deflater;
-
-public class UiFactory extends RecentsUiFactory {
-
-    /**
-     * Reusable command for applying the back button alpha on the background thread.
-     */
-    public static final UiThreadHelper.AsyncCommand SET_BACK_BUTTON_ALPHA =
-            (context, arg1, arg2) -> {
-        SystemUiProxy.INSTANCE.get(context).setBackButtonAlpha(Float.intBitsToFloat(arg1),
-                arg2 != 0);
-    };
-
-    public static Runnable enableLiveUIChanges(Launcher launcher) {
-        NavigationModeChangeListener listener = m -> {
-            launcher.getDragLayer().recreateControllers();
-            launcher.getRotationHelper().setRotationHadDifferentUI(m != Mode.NO_BUTTON);
-        };
-        SysUINavigationMode mode = SysUINavigationMode.INSTANCE.get(launcher);
-        SysUINavigationMode.Mode m = mode.addModeChangeListener(listener);
-        launcher.getRotationHelper().setRotationHadDifferentUI(m != Mode.NO_BUTTON);
-        return () -> mode.removeModeChangeListener(listener);
-    }
-
-    public static StateHandler[] getStateHandler(Launcher launcher) {
-        return new StateHandler[] {
-                launcher.getAllAppsController(),
-                launcher.getWorkspace(),
-                createRecentsViewStateController(launcher),
-                new BackButtonAlphaHandler(launcher)};
-    }
-
-    /**
-     * Sets the back button visibility based on the current state/window focus.
-     */
-    public static void onLauncherStateOrFocusChanged(Launcher launcher) {
-        Mode mode = SysUINavigationMode.getMode(launcher);
-        boolean shouldBackButtonBeHidden = mode.hasGestures
-                && launcher != null
-                && launcher.getStateManager().getState().hideBackButton
-                && launcher.hasWindowFocus();
-        if (shouldBackButtonBeHidden) {
-            // Show the back button if there is a floating view visible.
-            shouldBackButtonBeHidden = AbstractFloatingView.getTopOpenViewWithType(launcher,
-                    TYPE_ALL & ~TYPE_HIDE_BACK_BUTTON) == null;
-        }
-        UiThreadHelper.setBackButtonAlphaAsync(launcher, UiFactory.SET_BACK_BUTTON_ALPHA,
-                shouldBackButtonBeHidden ? 0f : 1f, true /* animate */);
-        if (launcher != null && launcher.getDragLayer() != null) {
-            launcher.getRootView().setDisallowBackGesture(shouldBackButtonBeHidden);
-        }
-    }
-
-    public static void onCreate(Launcher launcher) {
-        if (!launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)) {
-            launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() {
-                @Override
-                public void onStateTransitionStart(LauncherState toState) {
-                }
-
-                @Override
-                public void onStateTransitionComplete(LauncherState finalState) {
-                    boolean swipeUpEnabled = SysUINavigationMode.INSTANCE.get(launcher).getMode()
-                            .hasGestures;
-                    LauncherState prevState = launcher.getStateManager().getLastState();
-
-                    if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled
-                            && finalState == ALL_APPS && prevState == NORMAL) || BOUNCE_MAX_COUNT <=
-                            launcher.getSharedPrefs().getInt(HOME_BOUNCE_COUNT, 0))) {
-                        launcher.getSharedPrefs().edit().putBoolean(HOME_BOUNCE_SEEN, true).apply();
-                        launcher.getStateManager().removeStateListener(this);
-                    }
-                }
-            });
-        }
-
-        if (!launcher.getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false)) {
-            launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() {
-                @Override
-                public void onStateTransitionStart(LauncherState toState) {
-                }
-
-                @Override
-                public void onStateTransitionComplete(LauncherState finalState) {
-                    LauncherState prevState = launcher.getStateManager().getLastState();
-
-                    if ((finalState == ALL_APPS && prevState == OVERVIEW) || BOUNCE_MAX_COUNT <=
-                            launcher.getSharedPrefs().getInt(SHELF_BOUNCE_COUNT, 0)) {
-                        launcher.getSharedPrefs().edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply();
-                        launcher.getStateManager().removeStateListener(this);
-                    }
-                }
-            });
-        }
-    }
-
-    public static void onEnterAnimationComplete(Context context) {
-        // After the transition to home, enable the high-res thumbnail loader if it wasn't enabled
-        // as a part of quickstep, so that high-res thumbnails can load the next time we enter
-        // overview
-        RecentsModel.INSTANCE.get(context).getThumbnailCache()
-                .getHighResLoadingState().setVisible(true);
-    }
-
-    public static void onTrimMemory(Context context, int level) {
-        RecentsModel model = RecentsModel.INSTANCE.get(context);
-        if (model != null) {
-            model.onTrimMemory(level);
-        }
-    }
-
-    public static void useFadeOutAnimationForLauncherStart(Launcher launcher,
-            CancellationSignal cancellationSignal) {
-        QuickstepAppTransitionManagerImpl appTransitionManager =
-                (QuickstepAppTransitionManagerImpl) launcher.getAppTransitionManager();
-        appTransitionManager.setRemoteAnimationProvider((appTargets, wallpaperTargets) -> {
-
-            // On the first call clear the reference.
-            cancellationSignal.cancel();
-
-            ValueAnimator fadeAnimation = ValueAnimator.ofFloat(1, 0);
-            fadeAnimation.addUpdateListener(new RemoteFadeOutAnimationListener(appTargets,
-                    wallpaperTargets));
-            AnimatorSet anim = new AnimatorSet();
-            anim.play(fadeAnimation);
-            return anim;
-        }, cancellationSignal);
-    }
-
-    public static boolean dumpActivity(Activity activity, PrintWriter writer) {
-        if (!Utilities.IS_DEBUG_DEVICE) {
-            return false;
-        }
-        ByteArrayOutputStream out = new ByteArrayOutputStream();
-        if (!(new ActivityCompat(activity).encodeViewHierarchy(out))) {
-            return false;
-        }
-
-        Deflater deflater = new Deflater();
-        deflater.setInput(out.toByteArray());
-        deflater.finish();
-
-        out.reset();
-        byte[] buffer = new byte[1024];
-        while (!deflater.finished()) {
-            int count = deflater.deflate(buffer); // returns the generated code... index
-            out.write(buffer, 0, count);
-        }
-
-        writer.println("--encoded-view-dump-v0--");
-        writer.println(Base64.encodeToString(
-                out.toByteArray(), Base64.NO_WRAP | Base64.NO_PADDING));
-        return true;
-    }
-
-    public static boolean startIntentSenderForResult(Activity activity, IntentSender intent,
-            int requestCode, Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
-            Bundle options) {
-        StartActivityParams params = new StartActivityParams(activity, requestCode);
-        params.intentSender = intent;
-        params.fillInIntent = fillInIntent;
-        params.flagsMask = flagsMask;
-        params.flagsValues = flagsValues;
-        params.extraFlags = extraFlags;
-        params.options = options;
-        ((Context) activity).startActivity(ProxyActivityStarter.getLaunchIntent(activity, params));
-        return true;
-    }
-
-    public static boolean startActivityForResult(Activity activity, Intent intent, int requestCode,
-            Bundle options) {
-        StartActivityParams params = new StartActivityParams(activity, requestCode);
-        params.intent = intent;
-        params.options = options;
-        activity.startActivity(ProxyActivityStarter.getLaunchIntent(activity, params));
-        return true;
-    }
-
-    /**
-     * Removes any active ProxyActivityStarter task and sends RESULT_CANCELED to Launcher.
-     *
-     * ProxyActivityStarter is started with clear task to reset the task after which it removes the
-     * task itself.
-     */
-    public static void resetPendingActivityResults(Launcher launcher, int requestCode) {
-        launcher.onActivityResult(requestCode, RESULT_CANCELED, null);
-        launcher.startActivity(ProxyActivityStarter.getLaunchIntent(launcher, null));
-    }
-
-    public static ScaleAndTranslation getOverviewScaleAndTranslationForNormalState(Launcher l) {
-        if (SysUINavigationMode.getMode(l) == Mode.NO_BUTTON) {
-            float offscreenTranslationX = l.getDeviceProfile().widthPx
-                    - l.getOverviewPanel().getPaddingStart();
-            return new ScaleAndTranslation(1f, offscreenTranslationX, 0f);
-        }
-        return new ScaleAndTranslation(1.1f, 0f, 0f);
-    }
-
-    public static Person[] getPersons(ShortcutInfo si) {
-        Person[] persons = si.getPersons();
-        return persons == null ? Utilities.EMPTY_PERSON_ARRAY : persons;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java
index bb72315..3cb0088 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeEdgeSwipeController.java
@@ -11,10 +11,10 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager.AnimationComponents;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.quickstep.RecentsModel;
+import com.android.quickstep.SystemUiProxy;
 
 /**
  * Touch controller for handling edge swipes in landscape/seascape UI
@@ -24,7 +24,7 @@
     private static final String TAG = "LandscapeEdgeSwipeCtrl";
 
     public LandscapeEdgeSwipeController(Launcher l) {
-        super(l, SwipeDetector.HORIZONTAL);
+        super(l, SingleAxisSwipeDetector.HORIZONTAL);
     }
 
     @Override
@@ -73,7 +73,7 @@
     protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
         super.onSwipeInteractionCompleted(targetState, logAction);
         if (mStartState == NORMAL && targetState == OVERVIEW) {
-            RecentsModel.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
+            SystemUiProxy.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
         }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index ef6a5e2..99b2a81 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -43,11 +43,10 @@
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.uioverrides.states.OverviewState;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.quickstep.RecentsModel;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TouchInteractionService;
 import com.android.quickstep.util.LayoutUtils;
@@ -79,7 +78,7 @@
     private boolean mFinishFastOnSecondTouch;
 
     public PortraitStatesTouchController(Launcher l, boolean allowDragToOverview) {
-        super(l, SwipeDetector.VERTICAL);
+        super(l, SingleAxisSwipeDetector.VERTICAL);
         mOverviewPortraitStateTouchHelper = new PortraitOverviewStateTouchHelper(l);
         mAllowDragToOverview = allowDragToOverview;
     }
@@ -300,7 +299,7 @@
     protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
         super.onSwipeInteractionCompleted(targetState, logAction);
         if (mStartState == NORMAL && targetState == OVERVIEW) {
-            RecentsModel.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
+            SystemUiProxy.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/BaseRecentsActivity.java b/quickstep/src/com/android/quickstep/BaseRecentsActivity.java
index 71833ad..5fcdc19 100644
--- a/quickstep/src/com/android/quickstep/BaseRecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/BaseRecentsActivity.java
@@ -27,7 +27,6 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.R;
-import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.util.ActivityTracker;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.Themes;
@@ -122,13 +121,17 @@
     @Override
     public void onEnterAnimationComplete() {
         super.onEnterAnimationComplete();
-        UiFactory.onEnterAnimationComplete(this);
+        // After the transition to home, enable the high-res thumbnail loader if it wasn't enabled
+        // as a part of quickstep, so that high-res thumbnails can load the next time we enter
+        // overview
+        RecentsModel.INSTANCE.get(this).getThumbnailCache()
+                .getHighResLoadingState().setVisible(true);
     }
 
     @Override
     public void onTrimMemory(int level) {
         super.onTrimMemory(level);
-        UiFactory.onTrimMemory(this, level);
+        RecentsModel.INSTANCE.get(this).onTrimMemory(level);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
index 918645d..3e84e7d 100644
--- a/quickstep/src/com/android/quickstep/InputConsumer.java
+++ b/quickstep/src/com/android/quickstep/InputConsumer.java
@@ -33,7 +33,7 @@
     int TYPE_SCREEN_PINNED = 1 << 6;
     int TYPE_OVERVIEW_WITHOUT_FOCUS = 1 << 7;
     int TYPE_RESET_GESTURE = 1 << 8;
-    int TYPE_QUICK_CAPTURE = 1 << 9;
+    int TYPE_OVERSCROLL = 1 << 9;
 
     String[] NAMES = new String[] {
            "TYPE_NO_OP",                    // 0
@@ -45,7 +45,7 @@
             "TYPE_SCREEN_PINNED",           // 6
             "TYPE_OVERVIEW_WITHOUT_FOCUS",  // 7
             "TYPE_RESET_GESTURE",           // 8
-            "TYPE_QUICK_CAPTURE",           // 9
+            "TYPE_OVERSCROLL",              // 9
     };
 
     InputConsumer NO_OP = () -> TYPE_NO_OP;
diff --git a/quickstep/src/com/android/quickstep/NormalizedIconLoader.java b/quickstep/src/com/android/quickstep/NormalizedIconLoader.java
deleted file mode 100644
index bd6204a..0000000
--- a/quickstep/src/com/android/quickstep/NormalizedIconLoader.java
+++ /dev/null
@@ -1,99 +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.quickstep;
-
-import android.annotation.TargetApi;
-import android.app.ActivityManager.TaskDescription;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.UserHandle;
-import android.util.LruCache;
-import android.util.SparseArray;
-
-import com.android.launcher3.FastBitmapDrawable;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.graphics.DrawableFactory;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.systemui.shared.recents.model.IconLoader;
-import com.android.systemui.shared.recents.model.TaskKeyLruCache;
-
-/**
- * Extension of {@link IconLoader} with icon normalization support
- */
-@TargetApi(Build.VERSION_CODES.O)
-public class NormalizedIconLoader extends IconLoader {
-
-    private final SparseArray<BitmapInfo> mDefaultIcons = new SparseArray<>();
-    private final DrawableFactory mDrawableFactory;
-    private final boolean mDisableColorExtraction;
-
-    public NormalizedIconLoader(Context context, TaskKeyLruCache<Drawable> iconCache,
-            LruCache<ComponentName, ActivityInfo> activityInfoCache,
-            boolean disableColorExtraction) {
-        super(context, iconCache, activityInfoCache);
-        mDrawableFactory = DrawableFactory.INSTANCE.get(context);
-        mDisableColorExtraction = disableColorExtraction;
-    }
-
-    @Override
-    public Drawable getDefaultIcon(int userId) {
-        synchronized (mDefaultIcons) {
-            BitmapInfo info = mDefaultIcons.get(userId);
-            if (info == null) {
-                info = getBitmapInfo(Resources.getSystem()
-                        .getDrawable(android.R.drawable.sym_def_app_icon), userId, 0, false);
-                mDefaultIcons.put(userId, info);
-            }
-
-            return new FastBitmapDrawable(info);
-        }
-    }
-
-    @Override
-    protected Drawable createBadgedDrawable(Drawable drawable, int userId, TaskDescription desc) {
-        return new FastBitmapDrawable(getBitmapInfo(drawable, userId, desc.getPrimaryColor(),
-                false));
-    }
-
-    private BitmapInfo getBitmapInfo(Drawable drawable, int userId,
-            int primaryColor, boolean isInstantApp) {
-        try (LauncherIcons la = LauncherIcons.obtain(mContext)) {
-            if (mDisableColorExtraction) {
-                la.disableColorExtraction();
-            }
-            la.setWrapperBackgroundColor(primaryColor);
-
-            // User version code O, so that the icon is always wrapped in an adaptive icon container
-            return la.createBadgedIconBitmap(drawable, UserHandle.of(userId),
-                    Build.VERSION_CODES.O, isInstantApp);
-        }
-    }
-
-    @Override
-    protected Drawable getBadgedActivityIcon(ActivityInfo activityInfo, int userId,
-            TaskDescription desc) {
-        BitmapInfo bitmapInfo = getBitmapInfo(
-                activityInfo.loadUnbadgedIcon(mContext.getPackageManager()),
-                userId,
-                desc.getPrimaryColor(),
-                activityInfo.applicationInfo.isInstantApp());
-        return mDrawableFactory.newIcon(mContext, bitmapInfo, activityInfo);
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index e6e3297..1855e64 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS;
+import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
@@ -43,6 +44,7 @@
 import android.graphics.Region;
 import android.os.Process;
 import android.text.TextUtils;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.Surface;
 
@@ -52,7 +54,9 @@
 import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.DefaultDisplay;
+import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -65,7 +69,7 @@
  * Manages the state of the system during a swipe up gesture.
  */
 public class RecentsAnimationDeviceState implements
-        SysUINavigationMode.NavigationModeChangeListener,
+        NavigationModeChangeListener,
         DefaultDisplay.DisplayInfoChangeListener {
 
     private Context mContext;
@@ -74,6 +78,8 @@
     private DefaultDisplay mDefaultDisplay;
     private int mDisplayId;
 
+    private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();
+
     private @SystemUiStateFlags int mSystemUiStateFlags;
     private SysUINavigationMode.Mode mMode = THREE_BUTTONS;
 
@@ -114,6 +120,7 @@
             mContext.registerReceiver(mUserUnlockedReceiver,
                     new IntentFilter(ACTION_USER_UNLOCKED));
         }
+        runOnDestroy(() -> Utilities.unregisterReceiverSafely(mContext, mUserUnlockedReceiver));
 
         // Register for exclusion updates
         mExclusionListener = new SystemGestureExclusionListenerCompat(mDisplayId) {
@@ -124,7 +131,11 @@
                 mExclusionRegion = region;
             }
         };
+        runOnDestroy(mExclusionListener::unregister);
+
+        // Register for navigation mode changes
         onNavigationModeChanged(mSysUiNavMode.addModeChangeListener(this));
+        runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(this));
 
         // Add any blocked activities
         String blockingActivity = context.getString(R.string.gesture_blocking_activity);
@@ -133,18 +144,34 @@
         }
     }
 
+    private void runOnDestroy(Runnable action) {
+        mOnDestroyActions.add(action);
+    }
+
     /**
      * Cleans up all the registered listeners and receivers.
      */
     public void destroy() {
-        Utilities.unregisterReceiverSafely(mContext, mUserUnlockedReceiver);
-        mSysUiNavMode.removeModeChangeListener(this);
+        for (Runnable r : mOnDestroyActions) {
+            r.run();
+        }
         mDefaultDisplay.removeChangeListener(this);
-        mExclusionListener.unregister();
+    }
+
+    /**
+     * Adds a listener for the nav mode change, guaranteed to be called after the device state's
+     * mode has changed.
+     */
+    public void addNavigationModeChangedCallback(NavigationModeChangeListener listener) {
+        listener.onNavigationModeChanged(mSysUiNavMode.addModeChangeListener(listener));
+        runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(listener));
     }
 
     @Override
     public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "onNavigationModeChanged " + newMode);
+        }
         mDefaultDisplay.removeChangeListener(this);
         if (newMode.hasGestures) {
             mDefaultDisplay.addChangeListener(this);
@@ -168,6 +195,34 @@
     }
 
     /**
+     * @return the current navigation mode for the device.
+     */
+    public SysUINavigationMode.Mode getNavMode() {
+        return mMode;
+    }
+
+    /**
+     * @return whether the current nav mode is fully gestural.
+     */
+    public boolean isFullyGesturalNavMode() {
+        return mMode == NO_BUTTON;
+    }
+
+    /**
+     * @return whether the current nav mode has some gestures (either 2 or 0 button mode).
+     */
+    public boolean isGesturalNavMode() {
+        return mMode == TWO_BUTTONS || mMode == NO_BUTTON;
+    }
+
+    /**
+     * @return whether the current nav mode is button-based.
+     */
+    public boolean isButtonNavMode() {
+        return mMode == THREE_BUTTONS;
+    }
+
+    /**
      * @return the display id for the display that Launcher is running on.
      */
     public int getDisplayId() {
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 465d464..f248423 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -25,11 +25,9 @@
 import android.app.ActivityManager;
 import android.content.ComponentCallbacks2;
 import android.content.Context;
-import android.content.pm.LauncherApps;
 import android.os.Build;
 import android.os.Looper;
 import android.os.Process;
-import android.os.UserHandle;
 
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.systemui.shared.recents.model.Task;
@@ -48,8 +46,6 @@
 @TargetApi(Build.VERSION_CODES.O)
 public class RecentsModel extends TaskStackChangeListener {
 
-    private static final String TAG = "RecentsModel";
-
     // We do not need any synchronization for this variable as its only written on UI thread.
     public static final MainThreadInitializedObject<RecentsModel> INSTANCE =
             new MainThreadInitializedObject<>(RecentsModel::new);
@@ -70,7 +66,6 @@
         mIconCache = new TaskIconCache(context, looper);
         mThumbnailCache = new TaskThumbnailCache(context, looper);
         ActivityManagerWrapper.getInstance().registerTaskStackListener(this);
-        setupPackageListener();
     }
 
     public TaskIconCache getIconCache() {
@@ -183,35 +178,6 @@
         }
     }
 
-    public void onOverviewShown(boolean fromHome, String tag) {
-        SystemUiProxy.INSTANCE.get(mContext).onOverviewShown(fromHome, tag);
-    }
-
-    private void setupPackageListener() {
-        mContext.getSystemService(LauncherApps.class).registerCallback(new LauncherApps.Callback() {
-            @Override
-            public void onPackageRemoved(String packageName, UserHandle user) {
-                mIconCache.invalidatePackage(packageName);
-            }
-
-            @Override
-            public void onPackageChanged(String packageName, UserHandle user) {
-                mIconCache.invalidatePackage(packageName);
-            }
-
-            @Override
-            public void onPackageAdded(String packageName, UserHandle user) { }
-
-            @Override
-            public void onPackagesAvailable(
-                    String[] packageNames, UserHandle user, boolean replacing) { }
-
-            @Override
-            public void onPackagesUnavailable(
-                    String[] packageNames, UserHandle user, boolean replacing) { }
-        });
-    }
-
     public void addThumbnailChangeListener(TaskThumbnailChangeListener listener) {
         mThumbnailChangeListeners.add(listener);
     }
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 6873899..e3e8ace 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -36,7 +36,6 @@
     private RecentsAnimationTargets mTargets;
     // Temporary until we can hook into gesture state events
     private GestureState mLastGestureState;
-    private ThumbnailData mCanceledThumbnail;
 
     /**
      * Preloads the recents animation.
@@ -81,15 +80,15 @@
                 if (thumbnailData != null) {
                     // If a screenshot is provided, switch to the screenshot before cleaning up
                     activityInterface.switchRunningTaskViewToScreenshot(thumbnailData,
-                            () -> cleanUpRecentsAnimation());
+                            () -> cleanUpRecentsAnimation(thumbnailData));
                 } else {
-                    cleanUpRecentsAnimation();
+                    cleanUpRecentsAnimation(null /* canceledThumbnail */);
                 }
             }
 
             @Override
             public void onRecentsAnimationFinished(RecentsAnimationController controller) {
-                cleanUpRecentsAnimation();
+                cleanUpRecentsAnimation(null /* canceledThumbnail */);
             }
         });
         mCallbacks.addListener(gestureState);
@@ -119,7 +118,7 @@
             Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), toHome
                     ? mController::finishAnimationToHome
                     : mController::finishAnimationToApp);
-            cleanUpRecentsAnimation();
+            cleanUpRecentsAnimation(null /* canceledThumbnail */);
         }
     }
 
@@ -146,9 +145,9 @@
     /**
      * Cleans up the recents animation entirely.
      */
-    private void cleanUpRecentsAnimation() {
+    private void cleanUpRecentsAnimation(ThumbnailData canceledThumbnail) {
         // Clean up the screenshot if necessary
-        if (mController != null && mCanceledThumbnail != null) {
+        if (mController != null && canceledThumbnail != null) {
             mController.cleanupScreenshot();
         }
 
@@ -165,7 +164,6 @@
         mController = null;
         mCallbacks = null;
         mTargets = null;
-        mCanceledThumbnail = null;
         mLastGestureState = null;
     }
 
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index 289a129..873f29c 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -15,67 +15,61 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.uioverrides.RecentsUiFactory.GO_LOW_RAM_RECENTS_ENABLED;
+import static com.android.launcher3.uioverrides.QuickstepLauncher.GO_LOW_RAM_RECENTS_ENABLED;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
-import android.content.ComponentName;
+import android.app.ActivityManager.TaskDescription;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
-import android.util.LruCache;
+import android.os.UserHandle;
+import android.util.SparseArray;
 import android.view.accessibility.AccessibilityManager;
 
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.graphics.DrawableFactory;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.util.Preconditions;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskKey;
 import com.android.systemui.shared.recents.model.TaskKeyLruCache;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.PackageManagerWrapper;
 
-import java.util.Map;
 import java.util.function.Consumer;
 
 /**
  * Manages the caching of task icons and related data.
- * TODO(b/138944598): This class should later be merged into IconCache.
  */
 public class TaskIconCache {
 
     private final Handler mBackgroundHandler;
     private final AccessibilityManager mAccessibilityManager;
 
-    private final NormalizedIconLoader mIconLoader;
-
-    private final TaskKeyLruCache<Drawable> mIconCache;
-    private final TaskKeyLruCache<String> mContentDescriptionCache;
-    private final LruCache<ComponentName, ActivityInfo> mActivityInfoCache;
-
-    private TaskKeyLruCache.EvictionCallback mClearActivityInfoOnEviction =
-            new TaskKeyLruCache.EvictionCallback() {
-        @Override
-        public void onEntryEvicted(Task.TaskKey key) {
-            if (key != null) {
-                mActivityInfoCache.remove(key.getComponent());
-            }
-        }
-    };
+    private final Context mContext;
+    private final TaskKeyLruCache<TaskCacheEntry> mIconCache;
+    private final SparseArray<BitmapInfo> mDefaultIcons = new SparseArray<>();
 
     public TaskIconCache(Context context, Looper backgroundLooper) {
+        mContext = context;
         mBackgroundHandler = new Handler(backgroundLooper);
         mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
 
         Resources res = context.getResources();
         int cacheSize = res.getInteger(R.integer.recentsIconCacheSize);
-        mIconCache = new TaskKeyLruCache<>(cacheSize, mClearActivityInfoOnEviction);
-        mContentDescriptionCache = new TaskKeyLruCache<>(cacheSize, mClearActivityInfoOnEviction);
-        mActivityInfoCache = new LruCache<>(cacheSize);
-        mIconLoader = new NormalizedIconLoader(context, mIconCache, mActivityInfoCache,
-                true /* disableColorExtraction */);
+        mIconCache = new TaskKeyLruCache<>(cacheSize);
     }
 
     /**
@@ -96,15 +90,14 @@
         IconLoadRequest request = new IconLoadRequest(mBackgroundHandler) {
             @Override
             public void run() {
-                Drawable icon = mIconLoader.getIcon(task);
-                String contentDescription = loadContentDescriptionInBackground(task);
+                TaskCacheEntry entry = getCacheEntry(task);
                 if (isCanceled()) {
                     // We don't call back to the provided callback in this case
                     return;
                 }
                 MAIN_EXECUTOR.execute(() -> {
-                    task.icon = icon;
-                    task.titleDescription = contentDescription;
+                    task.icon = entry.icon;
+                    task.titleDescription = entry.contentDescription;
                     callback.accept(task);
                     onEnd();
                 });
@@ -116,51 +109,94 @@
 
     public void clear() {
         mIconCache.evictAll();
-        mContentDescriptionCache.evictAll();
     }
 
-    /**
-     * Loads the content description for the given {@param task}.
-     */
-    private String loadContentDescriptionInBackground(Task task) {
-        // Return the cached content description if it exists
-        String label = mContentDescriptionCache.getAndInvalidateIfModified(task.key);
-        if (label != null) {
-            return label;
-        }
-
-        // Skip loading content descriptions if accessibility is disabled unless low RAM recents
-        // is enabled.
-        if (!GO_LOW_RAM_RECENTS_ENABLED && !mAccessibilityManager.isEnabled()) {
-            return "";
-        }
-
-        // Skip loading the content description if the activity no longer exists
-        ActivityInfo activityInfo = mIconLoader.getAndUpdateActivityInfo(task.key);
-        if (activityInfo == null) {
-            return "";
-        }
-
-        // Load the label otherwise
-        label = ActivityManagerWrapper.getInstance().getBadgedContentDescription(activityInfo,
-                task.key.userId, task.taskDescription);
-        mContentDescriptionCache.put(task.key, label);
-        return label;
-    }
-
-
     void onTaskRemoved(TaskKey taskKey) {
         mIconCache.remove(taskKey);
     }
 
-    void invalidatePackage(String packageName) {
-        // TODO(b/138944598): Merge this class into IconCache so we can do this at the base level
-        Map<ComponentName, ActivityInfo> activityInfoCache = mActivityInfoCache.snapshot();
-        for (ComponentName cn : activityInfoCache.keySet()) {
-            if (cn.getPackageName().equals(packageName)) {
-                mActivityInfoCache.remove(cn);
+    @WorkerThread
+    private TaskCacheEntry getCacheEntry(Task task) {
+        TaskCacheEntry entry = mIconCache.getAndInvalidateIfModified(task.key);
+        if (entry != null) {
+            return entry;
+        }
+
+        TaskDescription desc = task.taskDescription;
+        TaskKey key = task.key;
+        ActivityInfo activityInfo = null;
+
+        // Create new cache entry
+        entry = new TaskCacheEntry();
+
+        // Load icon
+        // TODO: Load icon resource (b/143363444)
+        Bitmap icon = desc.getIcon();
+        if (icon != null) {
+            entry.icon = new FastBitmapDrawable(getBitmapInfo(
+                    new BitmapDrawable(mContext.getResources(), icon),
+                    key.userId,
+                    desc.getPrimaryColor(),
+                    false /* isInstantApp */));
+        } else {
+            activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
+                    key.getComponent(), key.userId);
+            if (activityInfo != null) {
+                BitmapInfo bitmapInfo = getBitmapInfo(
+                        activityInfo.loadUnbadgedIcon(mContext.getPackageManager()),
+                        key.userId,
+                        desc.getPrimaryColor(),
+                        activityInfo.applicationInfo.isInstantApp());
+                entry.icon = DrawableFactory.INSTANCE.get(mContext).newIcon(
+                        mContext, bitmapInfo, activityInfo);
+            } else {
+                entry.icon = getDefaultIcon(key.userId);
             }
         }
+
+        // Loading content descriptions if accessibility or low RAM recents is enabled.
+        if (GO_LOW_RAM_RECENTS_ENABLED || mAccessibilityManager.isEnabled()) {
+            // Skip loading the content description if the activity no longer exists
+            if (activityInfo == null) {
+                activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
+                        key.getComponent(), key.userId);
+            }
+            if (activityInfo != null) {
+                entry.contentDescription = ActivityManagerWrapper.getInstance()
+                        .getBadgedContentDescription(activityInfo, task.key.userId,
+                                task.taskDescription);
+            }
+        }
+
+        mIconCache.put(task.key, entry);
+        return entry;
+    }
+
+    @WorkerThread
+    private Drawable getDefaultIcon(int userId) {
+        synchronized (mDefaultIcons) {
+            BitmapInfo info = mDefaultIcons.get(userId);
+            if (info == null) {
+                try (LauncherIcons la = LauncherIcons.obtain(mContext)) {
+                    info = la.makeDefaultIcon(UserHandle.of(userId));
+                }
+                mDefaultIcons.put(userId, info);
+            }
+            return new FastBitmapDrawable(info);
+        }
+    }
+
+    @WorkerThread
+    private BitmapInfo getBitmapInfo(Drawable drawable, int userId,
+            int primaryColor, boolean isInstantApp) {
+        try (LauncherIcons la = LauncherIcons.obtain(mContext)) {
+            la.disableColorExtraction();
+            la.setWrapperBackgroundColor(primaryColor);
+
+            // User version code O, so that the icon is always wrapped in an adaptive icon container
+            return la.createBadgedIconBitmap(drawable, UserHandle.of(userId),
+                    Build.VERSION_CODES.O, isInstantApp);
+        }
     }
 
     public static abstract class IconLoadRequest extends HandlerRunnable {
@@ -168,4 +204,9 @@
             super(handler, null);
         }
     }
+
+    private static class TaskCacheEntry {
+        public Drawable icon;
+        public String contentDescription = "";
+    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index b17ed4c..13731b6 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -31,6 +31,9 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.rule.ShellCommandRule.disableHeadsUpNotification;
 import static com.android.launcher3.util.rule.ShellCommandRule.getLauncherCommand;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_PRESUBMIT;
+import static com.android.launcher3.util.rule.TestStabilityRule.RUN_FLAFOR;
+import static com.android.launcher3.util.rule.TestStabilityRule.UNBUNDLED_PRESUBMIT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -99,7 +102,7 @@
         }
 
         mOrderSensitiveRules = RuleChain.outerRule(new NavigationModeSwitchRule(mLauncher))
-                        .around(new FailureWatcher(mDevice));
+                .around(new FailureWatcher(mDevice));
 
         mOtherLauncherActivity = context.getPackageManager().queryIntentActivities(
                 getHomeIntentInPackage(context),
@@ -131,7 +134,8 @@
     @Test
     public void goToOverviewFromHome() {
         // b/142828227
-        if (android.os.Build.MODEL.contains("Cuttlefish") && TestHelpers.isInLauncherProcess()) {
+        if (android.os.Build.MODEL.contains("Cuttlefish") && TestHelpers.isInLauncherProcess() &&
+                (RUN_FLAFOR & (PLATFORM_PRESUBMIT | UNBUNDLED_PRESUBMIT)) != 0) {
             return;
         }
         mDevice.pressHome();
@@ -145,7 +149,8 @@
     @Test
     public void goToOverviewFromApp() {
         // b/142828227
-        if (android.os.Build.MODEL.contains("Cuttlefish") && TestHelpers.isInLauncherProcess()) {
+        if (android.os.Build.MODEL.contains("Cuttlefish") && TestHelpers.isInLauncherProcess() &&
+                (RUN_FLAFOR & (PLATFORM_PRESUBMIT | UNBUNDLED_PRESUBMIT)) != 0) {
             return;
         }
         startAppFastAndWaitForRecentTask(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
@@ -183,7 +188,8 @@
     @Test
     public void testOverview() {
         // b/142828227
-        if (android.os.Build.MODEL.contains("Cuttlefish") && TestHelpers.isInLauncherProcess()) {
+        if (android.os.Build.MODEL.contains("Cuttlefish") && TestHelpers.isInLauncherProcess() &&
+                (RUN_FLAFOR & (PLATFORM_PRESUBMIT | UNBUNDLED_PRESUBMIT)) != 0) {
             return;
         }
         startAppFastAndWaitForRecentTask(getAppPackageName());
diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
index 25224b0..50cab74 100644
--- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
@@ -18,6 +18,9 @@
 
 import static com.android.launcher3.util.RaceConditionReproducer.enterEvt;
 import static com.android.launcher3.util.RaceConditionReproducer.exitEvt;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_PRESUBMIT;
+import static com.android.launcher3.util.rule.TestStabilityRule.RUN_FLAFOR;
+import static com.android.launcher3.util.rule.TestStabilityRule.UNBUNDLED_PRESUBMIT;
 
 import android.content.Intent;
 
@@ -93,7 +96,8 @@
     @NavigationModeSwitch
     public void testStressSwipeToOverview() {
         // b/142828227
-        if (android.os.Build.MODEL.contains("Cuttlefish") && TestHelpers.isInLauncherProcess()) {
+        if (android.os.Build.MODEL.contains("Cuttlefish") && TestHelpers.isInLauncherProcess() &&
+                (RUN_FLAFOR & (PLATFORM_PRESUBMIT | UNBUNDLED_PRESUBMIT)) != 0) {
             return;
         }
         for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index d270d76..8cd3bb6 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -248,33 +248,32 @@
     @Test
     @NavigationModeSwitch
     @PortraitLandscape
-    @Ignore("Temporarily disabled b/140252765")
     public void testQuickSwitchFromApp() throws Exception {
-        startAppFast(getAppPackageName());
         startTestActivity(2);
-        String calculatorPackage = resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR);
-        startAppFast(calculatorPackage);
+        startTestActivity(3);
+        startTestActivity(4);
 
         Background background = getAndAssertBackground();
         background.quickSwitchToPreviousApp();
         assertTrue("The first app we should have quick switched to is not running",
-                isTestActivityRunning("TestActivity2"));
+                isTestActivityRunning(3));
 
         background = getAndAssertBackground();
         background.quickSwitchToPreviousApp();
         if (mLauncher.getNavigationModel() == NavigationModel.THREE_BUTTON) {
             // 3-button mode toggles between 2 apps, rather than going back further.
             assertTrue("Second quick switch should have returned to the first app.",
-                    mDevice.wait(Until.hasObject(By.pkg(calculatorPackage)), DEFAULT_UI_TIMEOUT));
+                    isTestActivityRunning(4));
         } else {
             assertTrue("The second app we should have quick switched to is not running",
-                    isTestActivityRunning("Test Pin Item"));
+                    isTestActivityRunning(2));
         }
         getAndAssertBackground();
     }
 
-    private boolean isTestActivityRunning(String activityLabel) {
-        return mDevice.wait(Until.hasObject(By.pkg(getAppPackageName()).text(activityLabel)),
+    private boolean isTestActivityRunning(int activityNumber) {
+        return mDevice.wait(Until.hasObject(By.pkg(getAppPackageName())
+                        .text("TestActivity" + activityNumber)),
                 DEFAULT_UI_TIMEOUT);
     }
 
@@ -285,7 +284,7 @@
         startTestActivity(2);
         mLauncher.pressHome().quickSwitchToPreviousApp();
         assertTrue("The most recent task is not running after quick switching from home",
-                isTestActivityRunning("TestActivity2"));
+                isTestActivityRunning(2));
         getAndAssertBackground();
     }
 }
diff --git a/res/anim/slide_in_right.xml b/res/anim/slide_in_right.xml
deleted file mode 100644
index 55d3e54..0000000
--- a/res/anim/slide_in_right.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shareInterpolator="false" >
-  <translate
-      android:duration="@android:integer/config_shortAnimTime"
-      android:fromXDelta="100%"
-      android:toXDelta="0%"
-      />
-</set>
\ No newline at end of file
diff --git a/res/values/config.xml b/res/values/config.xml
index 0387184..10671c5 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -69,7 +69,6 @@
     <string name="app_transition_manager_class" translatable="false"></string>
     <string name="instant_app_resolver_class" translatable="false"></string>
     <string name="main_process_initializer_class" translatable="false"></string>
-    <string name="system_shortcut_factory_class" translatable="false"></string>
     <string name="app_launch_tracker_class" translatable="false"></string>
     <string name="test_information_handler_class" translatable="false"></string>
     <string name="launcher_activity_logic_class" translatable="false"></string>
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index b28077f..382bfdf 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -35,7 +35,7 @@
 import com.android.launcher3.logging.StatsLogUtils.LogStateProvider;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.logging.UserEventDispatcher.UserEventDelegate;
-import com.android.launcher3.uioverrides.UiFactory;
+import com.android.launcher3.uioverrides.ApiWrapper;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.ViewCache;
@@ -81,19 +81,39 @@
     protected StatsLogManager mStatsLogManager;
     protected SystemUiController mSystemUiController;
 
-    private static final int ACTIVITY_STATE_STARTED = 1 << 0;
-    private static final int ACTIVITY_STATE_RESUMED = 1 << 1;
+
+    public static final int ACTIVITY_STATE_STARTED = 1 << 0;
+    public static final int ACTIVITY_STATE_RESUMED = 1 << 1;
+
     /**
-     * State flag indicating if the user is active or the actitvity when to background as a result
+     * State flags indicating that the activity has received one frame after resume, and was
+     * not immediately paused.
+     */
+    public static final int ACTIVITY_STATE_DEFERRED_RESUMED = 1 << 2;
+
+    public static final int ACTIVITY_STATE_WINDOW_FOCUSED = 1 << 3;
+
+    /**
+     * State flag indicating if the user is active or the activity when to background as a result
      * of user action.
      * @see #isUserActive()
      */
-    private static final int ACTIVITY_STATE_USER_ACTIVE = 1 << 2;
+    public static final int ACTIVITY_STATE_USER_ACTIVE = 1 << 4;
+
+    /**
+     * State flag indicating that a state transition is in progress
+     */
+    public static final int ACTIVITY_STATE_TRANSITION_ACTIVE = 1 << 5;
 
     @Retention(SOURCE)
     @IntDef(
             flag = true,
-            value = {ACTIVITY_STATE_STARTED, ACTIVITY_STATE_RESUMED, ACTIVITY_STATE_USER_ACTIVE})
+            value = {ACTIVITY_STATE_STARTED,
+                    ACTIVITY_STATE_RESUMED,
+                    ACTIVITY_STATE_DEFERRED_RESUMED,
+                    ACTIVITY_STATE_WINDOW_FOCUSED,
+                    ACTIVITY_STATE_USER_ACTIVE,
+                    ACTIVITY_STATE_TRANSITION_ACTIVE})
     public @interface ActivityFlags{}
 
     @ActivityFlags
@@ -146,19 +166,19 @@
 
     @Override
     protected void onStart() {
-        mActivityFlags |= ACTIVITY_STATE_STARTED;
+        addActivityFlags(ACTIVITY_STATE_STARTED);
         super.onStart();
     }
 
     @Override
     protected void onResume() {
-        mActivityFlags |= ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_USER_ACTIVE;
+        addActivityFlags(ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_USER_ACTIVE);
         super.onResume();
     }
 
     @Override
     protected void onUserLeaveHint() {
-        mActivityFlags &= ~ACTIVITY_STATE_USER_ACTIVE;
+        removeActivityFlags(ACTIVITY_STATE_USER_ACTIVE);
         super.onUserLeaveHint();
     }
 
@@ -172,7 +192,7 @@
 
     @Override
     protected void onStop() {
-        mActivityFlags &= ~ACTIVITY_STATE_STARTED & ~ACTIVITY_STATE_USER_ACTIVE;
+        removeActivityFlags(ACTIVITY_STATE_STARTED | ACTIVITY_STATE_USER_ACTIVE);
         mForceInvisible = 0;
         super.onStop();
 
@@ -183,7 +203,7 @@
 
     @Override
     protected void onPause() {
-        mActivityFlags &= ~ACTIVITY_STATE_RESUMED;
+        removeActivityFlags(ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_DEFERRED_RESUMED);
         super.onPause();
 
         // Reset the overridden sysui flags used for the task-swipe launch animation, we do this
@@ -193,6 +213,17 @@
         getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 0);
     }
 
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus) {
+        super.onWindowFocusChanged(hasFocus);
+        if (hasFocus) {
+            addActivityFlags(ACTIVITY_STATE_WINDOW_FOCUSED);
+        } else {
+            removeActivityFlags(ACTIVITY_STATE_WINDOW_FOCUSED);
+        }
+
+    }
+
     public boolean isStarted() {
         return (mActivityFlags & ACTIVITY_STATE_STARTED) != 0;
     }
@@ -208,6 +239,22 @@
         return (mActivityFlags & ACTIVITY_STATE_USER_ACTIVE) != 0;
     }
 
+    public int getActivityFlags() {
+        return mActivityFlags;
+    }
+
+    protected void addActivityFlags(int flags) {
+        mActivityFlags |= flags;
+        onActivityFlagsChanged(flags);
+    }
+
+    protected void removeActivityFlags(int flags) {
+        mActivityFlags &= ~flags;
+        onActivityFlagsChanged(flags);
+    }
+
+    protected void onActivityFlagsChanged(int changeBits) { }
+
     public void addOnDeviceProfileChangeListener(OnDeviceProfileChangeListener listener) {
         mDPChangeListeners.add(listener);
     }
@@ -233,7 +280,7 @@
     /**
      * Used to set the override visibility state, used only to handle the transition home with the
      * recents animation.
-     * @see QuickstepAppTransitionManagerImpl#getWallpaperOpenRunner()
+     * @see QuickstepAppTransitionManagerImpl#getWallpaperOpenRunner
      */
     public void addForceInvisibleFlag(@InvisibilityFlags int flag) {
         mForceInvisible |= flag;
@@ -260,7 +307,7 @@
 
     @Override
     public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
-        if (!UiFactory.dumpActivity(this, writer)) {
+        if (!ApiWrapper.dumpActivity(this, writer)) {
             super.dump(prefix, fd, writer, args);
         }
     }
diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
index 8ebf464..93def50 100644
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -454,6 +454,8 @@
                     .object()
                     .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0))
                     .key(NAME_KEY).value(name)
+                    .key(USER_HANDLE_KEY).value(
+                            UserManagerCompat.getInstance(mContext).getSerialNumberForUser(user))
                     .key(APP_SHORTCUT_TYPE_KEY).value(isActivity);
                 if (icon != null) {
                     byte[] iconByteArray = GraphicsUtils.flattenBitmap(icon);
@@ -475,7 +477,7 @@
 
         public Pair<ItemInfo, Object> getItemInfo() {
             if (isActivity) {
-                WorkspaceItemInfo si = createWorkspaceItemInfo(data,
+                WorkspaceItemInfo si = createWorkspaceItemInfo(data, user,
                         LauncherAppState.getInstance(mContext));
                 si.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
                 si.status |= WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
@@ -500,7 +502,7 @@
                 return Pair.create(widgetInfo, providerInfo);
             } else {
                 WorkspaceItemInfo itemInfo =
-                        createWorkspaceItemInfo(data, LauncherAppState.getInstance(mContext));
+                        createWorkspaceItemInfo(data, user, LauncherAppState.getInstance(mContext));
                 return Pair.create(itemInfo, null);
             }
         }
@@ -618,7 +620,8 @@
         return new PendingInstallShortcutInfo(info, original.mContext);
     }
 
-    private static WorkspaceItemInfo createWorkspaceItemInfo(Intent data, LauncherAppState app) {
+    private static WorkspaceItemInfo createWorkspaceItemInfo(Intent data, UserHandle user,
+            LauncherAppState app) {
         if (data == null) {
             Log.e(TAG, "Can't construct WorkspaceItemInfo with null data");
             return null;
@@ -635,10 +638,7 @@
         }
 
         final WorkspaceItemInfo info = new WorkspaceItemInfo();
-
-        // Only support intents for current user for now. Intents sent from other
-        // users wouldn't get here without intent forwarding anyway.
-        info.user = Process.myUserHandle();
+        info.user = user;
 
         BitmapInfo iconInfo = null;
         LauncherIcons li = LauncherIcons.obtain(app.getContext());
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 67c1a04..3dce9fc 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -31,6 +31,10 @@
 import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_LAUNCHER_LOAD;
 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
 import static com.android.launcher3.logging.LoggerUtils.newTarget;
+import static com.android.launcher3.popup.SystemShortcut.APP_INFO;
+import static com.android.launcher3.popup.SystemShortcut.DISMISS_PREDICTION;
+import static com.android.launcher3.popup.SystemShortcut.INSTALL;
+import static com.android.launcher3.popup.SystemShortcut.WIDGETS;
 import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
 import static com.android.launcher3.testing.TestProtocol.CRASH_ADD_CUSTOM_SHORTCUT;
 
@@ -58,6 +62,7 @@
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.Parcelable;
 import android.os.Process;
@@ -82,6 +87,8 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.DropTarget.DragObject;
+import com.android.launcher3.LauncherState.ScaleAndTranslation;
+import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsStore;
@@ -111,11 +118,12 @@
 import com.android.launcher3.pm.PinRequestHelper;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.qsb.QsbContainerView;
 import com.android.launcher3.states.RotationHelper;
 import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.touch.AllAppsSwipeController;
 import com.android.launcher3.touch.ItemClickHandler;
-import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
@@ -135,6 +143,7 @@
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.util.TouchController;
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.util.UiThreadHelper;
 import com.android.launcher3.util.ViewOnDrawExecutor;
@@ -166,6 +175,7 @@
 import java.util.List;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
+import java.util.stream.Stream;
 
 /**
  * Default launcher application.
@@ -294,12 +304,11 @@
      */
     private PendingRequestArgs mPendingRequestArgs;
     // Request id for any pending activity result
-    private int mPendingActivityRequestCode = -1;
+    protected int mPendingActivityRequestCode = -1;
 
     public ViewGroupFocusHelper mFocusHandler;
 
     private RotationHelper mRotationHelper;
-    private Runnable mCancelTouchController;
 
     final Handler mHandler = new Handler();
     private final Runnable mHandleDeferredResume = this::handleDeferredResume;
@@ -350,7 +359,6 @@
         mDragController = new DragController(this);
         mAllAppsController = new AllAppsTransitionController(this);
         mStateManager = new LauncherStateManager(this);
-        UiFactory.onCreate(this);
 
         mAppWidgetManager = AppWidgetManagerCompat.getInstance(this);
         mAppWidgetHost = new LauncherAppWidgetHost(this,
@@ -474,7 +482,6 @@
     @Override
     public void onEnterAnimationComplete() {
         super.onEnterAnimationComplete();
-        UiFactory.onEnterAnimationComplete(this);
         mAllAppsController.highlightWorkTabIfNecessary();
         mRotationHelper.setCurrentTransitionRequest(REQUEST_NONE);
     }
@@ -488,7 +495,6 @@
         }
 
         mOldConfig.setTo(newConfig);
-        UiFactory.onLauncherStateOrResumeChanged(this);
         super.onConfigurationChanged(newConfig);
     }
 
@@ -504,7 +510,7 @@
     public void reapplyUi() {
         if (supportsFakeLandscapeUI()) {
             mRotationMode = mStableDeviceProfile == null
-                    ? RotationMode.NORMAL : UiFactory.getRotationMode(mDeviceProfile);
+                    ? RotationMode.NORMAL : getFakeRotationMode(mDeviceProfile);
         }
         getRootView().dispatchInsets();
         getStateManager().reapplyState(true /* cancelCurrentAnimation */);
@@ -567,7 +573,7 @@
 
         if (supportsFakeLandscapeUI() && mDeviceProfile.isVerticalBarLayout()) {
             mStableDeviceProfile = mDeviceProfile.inv.portraitProfile;
-            mRotationMode = UiFactory.getRotationMode(mDeviceProfile);
+            mRotationMode = getFakeRotationMode(mDeviceProfile);
         } else {
             mStableDeviceProfile = null;
             mRotationMode = RotationMode.NORMAL;
@@ -933,8 +939,6 @@
         NotificationListener.removeNotificationsChangedListener();
         getStateManager().moveToRestState();
 
-        UiFactory.onLauncherStateOrResumeChanged(this);
-
         // Workaround for b/78520668, explicitly trim memory once UI is hidden
         onTrimMemory(TRIM_MEMORY_UI_HIDDEN);
     }
@@ -957,7 +961,6 @@
             logStopAndResume(Action.Command.RESUME);
             getUserEventDispatcher().startSession();
 
-            UiFactory.onLauncherStateOrResumeChanged(this);
             AppLaunchTracker.INSTANCE.get(this).onReturnedToHome();
 
             // Process any items that were added while Launcher was away.
@@ -972,15 +975,17 @@
 
             DiscoveryBounce.showForHomeIfNeeded(this);
 
-            if (mPendingActivityRequestCode != -1 && isInState(NORMAL)) {
-                UiFactory.resetPendingActivityResults(this, mPendingActivityRequestCode);
-            }
+            onDeferredResumed();
+            addActivityFlags(ACTIVITY_STATE_DEFERRED_RESUMED);
+
             mDeferredResumePending = false;
         } else {
             mDeferredResumePending = true;
         }
     }
 
+    protected void onDeferredResumed() { }
+
     private void logStopAndResume(int command) {
         int containerType = mStateManager.getState().containerType;
         if (containerType == ContainerType.WORKSPACE && mWorkspace != null) {
@@ -1034,12 +1039,14 @@
         if (mDeferOverlayCallbacks) {
             scheduleDeferredCheck();
         }
+        addActivityFlags(ACTIVITY_STATE_TRANSITION_ACTIVE);
     }
 
     public void onStateSetEnd(LauncherState state) {
         getAppWidgetHost().setResumed(state == LauncherState.NORMAL);
         getWorkspace().setClipChildren(!state.disablePageClipping);
         finishAutoCancelActionMode();
+        removeActivityFlags(ACTIVITY_STATE_TRANSITION_ACTIVE);
     }
 
     @Override
@@ -1084,18 +1091,6 @@
         }
     }
 
-    @Override
-    protected void onUserLeaveHint() {
-        super.onUserLeaveHint();
-        UiFactory.onLauncherStateOrResumeChanged(this);
-    }
-
-    @Override
-    public void onWindowFocusChanged(boolean hasFocus) {
-        super.onWindowFocusChanged(hasFocus);
-        mStateManager.onWindowFocusChanged();
-    }
-
     class LauncherOverlayCallbacksImpl implements LauncherOverlayCallbacks {
 
         public void onScrollChanged(float progress) {
@@ -1159,7 +1154,6 @@
 
         // Setup the drag layer
         mDragLayer.setup(mDragController, mWorkspace);
-        mCancelTouchController = UiFactory.enableLiveUIChanges(this);
 
         mWorkspace.setup(mDragController);
         // Until the workspace is bound, ensure that we keep the wallpaper offset locked to the
@@ -1537,11 +1531,6 @@
         mWorkspace.removeFolderListeners();
         PluginManagerWrapper.INSTANCE.get(this).removePluginListener(this);
 
-        if (mCancelTouchController != null) {
-            mCancelTouchController.run();
-            mCancelTouchController = null;
-        }
-
         // Stop callbacks from LauncherModel
         // It's possible to receive onDestroy after a new Launcher activity has
         // been created. In this case, don't interfere with the new Launcher.
@@ -1577,10 +1566,7 @@
         if (requestCode != -1) {
             mPendingActivityRequestCode = requestCode;
         }
-        if (requestCode == -1
-                || !UiFactory.startActivityForResult(this, intent, requestCode, options)) {
-            super.startActivityForResult(intent, requestCode, options);
-        }
+        super.startActivityForResult(intent, requestCode, options);
     }
 
     @Override
@@ -1589,14 +1575,11 @@
         if (requestCode != -1) {
             mPendingActivityRequestCode = requestCode;
         }
-        if (requestCode == -1 || !UiFactory.startIntentSenderForResult(this, intent, requestCode,
-                fillInIntent, flagsMask, flagsValues, extraFlags, options)) {
-            try {
-                super.startIntentSenderForResult(intent, requestCode,
-                        fillInIntent, flagsMask, flagsValues, extraFlags, options);
-            } catch (IntentSender.SendIntentException e) {
-                throw new ActivityNotFoundException();
-            }
+        try {
+            super.startIntentSenderForResult(intent, requestCode,
+                    fillInIntent, flagsMask, flagsValues, extraFlags, options);
+        } catch (IntentSender.SendIntentException e) {
+            throw new ActivityNotFoundException();
         }
     }
 
@@ -1934,7 +1917,6 @@
             // This clears all widget bitmaps from the widget tray
             // TODO(hyunyoungs)
         }
-        UiFactory.onTrimMemory(this, level);
     }
 
     @Override
@@ -2653,16 +2635,40 @@
         return super.onKeyUp(keyCode, event);
     }
 
-    public static Launcher getLauncher(Context context) {
-        return (Launcher) fromContext(context);
+    protected StateHandler[] createStateHandlers() {
+        return new StateHandler[] { getAllAppsController(), getWorkspace() };
     }
 
+    public TouchController[] createTouchControllers() {
+        return new TouchController[] {getDragController(), new AllAppsSwipeController(this)};
+    }
+
+    protected RotationMode getFakeRotationMode(DeviceProfile deviceProfile) {
+        return RotationMode.NORMAL;
+    }
+
+    protected ScaleAndTranslation getOverviewScaleAndTranslationForNormalState() {
+        return new ScaleAndTranslation(1.1f, 0f, 0f);
+    }
+
+    public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) { }
+
+    public void onDragLayerHierarchyChanged() { }
+
     @Override
     public void returnToHomescreen() {
         super.returnToHomescreen();
         getStateManager().goToState(LauncherState.NORMAL);
     }
 
+    public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
+        return Stream.of(APP_INFO, WIDGETS, INSTALL, DISMISS_PREDICTION);
+    }
+
+    public static Launcher getLauncher(Context context) {
+        return (Launcher) fromContext(context);
+    }
+
     /**
      * Just a wrapper around the type cast to allow easier tracking of calls.
      */
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index fc2e953..b0b213c 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -93,10 +93,6 @@
     private boolean mModelLoaded;
     public boolean isModelLoaded() {
         synchronized (mLock) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE,
-                        "isModelLoaded: " + mModelLoaded + ", " + mLoaderTask);
-            }
             return mModelLoaded && mLoaderTask == null;
         }
     }
@@ -326,9 +322,6 @@
     public void stopLoader() {
         synchronized (mLock) {
             LoaderTask oldTask = mLoaderTask;
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE, "LauncherModel.stopLoader");
-            }
             mLoaderTask = null;
             if (oldTask != null) {
                 oldTask.stopLocked();
@@ -340,10 +333,6 @@
         synchronized (mLock) {
             stopLoader();
             mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, sBgDataModel, results);
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE,
-                        "LauncherModel.startLoaderForResults " + mLoaderTask);
-            }
 
             // Always post the loader task, instead of running directly (even on same thread) so
             // that we exit any nested synchronized blocks
@@ -445,10 +434,6 @@
         public void close() {
             synchronized (mLock) {
                 // If we are still the last one to be scheduled, remove ourselves.
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE,
-                            "LauncherModel.close " + mLoaderTask + ", " + mTask);
-                }
                 if (mLoaderTask == mTask) {
                     mLoaderTask = null;
                 }
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 6e2626b..d2b447b 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -26,9 +26,11 @@
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_SCALE;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
 import static com.android.launcher3.anim.Interpolators.clampToProgress;
+import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
 import static com.android.launcher3.testing.TestProtocol.ALL_APPS_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.BACKGROUND_APP_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
@@ -36,14 +38,11 @@
 import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.SPRING_LOADED_STATE_ORDINAL;
-import static com.android.launcher3.anim.Interpolators.ACCEL_2;
-import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
 
 import android.view.animation.Interpolator;
 
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.states.SpringLoadedState;
-import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.uioverrides.states.AllAppsState;
 import com.android.launcher3.uioverrides.states.OverviewState;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -210,7 +209,7 @@
     }
 
     public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        return UiFactory.getOverviewScaleAndTranslationForNormalState(launcher);
+        return launcher.getOverviewScaleAndTranslationForNormalState();
     }
 
     public float getOverviewFullscreenProgress() {
diff --git a/src/com/android/launcher3/LauncherStateManager.java b/src/com/android/launcher3/LauncherStateManager.java
index 848e19f..daf270b 100644
--- a/src/com/android/launcher3/LauncherStateManager.java
+++ b/src/com/android/launcher3/LauncherStateManager.java
@@ -24,7 +24,8 @@
 import android.animation.AnimatorSet;
 import android.os.Handler;
 import android.os.Looper;
-import android.util.Log;
+
+import androidx.annotation.IntDef;
 
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
@@ -32,16 +33,12 @@
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.anim.PropertySetter.AnimatedPropertySetter;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.uioverrides.UiFactory;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 
-import androidx.annotation.IntDef;
-
 /**
  * TODO: figure out what kind of tests we can write for this
  *
@@ -146,7 +143,7 @@
 
     public StateHandler[] getStateHandlers() {
         if (mStateHandlers == null) {
-            mStateHandlers = UiFactory.getStateHandler(mLauncher);
+            mStateHandlers = mLauncher.createStateHandlers();
         }
         return mStateHandlers;
     }
@@ -414,7 +411,6 @@
             // Only disable clipping if needed, otherwise leave it as previous value.
             mLauncher.getWorkspace().setClipChildren(false);
         }
-        UiFactory.onLauncherStateOrResumeChanged(mLauncher);
 
         for (int i = mListeners.size() - 1; i >= 0; i--) {
             mListeners.get(i).onStateTransitionStart(state);
@@ -435,8 +431,6 @@
             setRestState(null);
         }
 
-        UiFactory.onLauncherStateOrResumeChanged(mLauncher);
-
         for (int i = mListeners.size() - 1; i >= 0; i--) {
             mListeners.get(i).onStateTransitionComplete(state);
         }
@@ -444,10 +438,6 @@
         AccessibilityManagerCompat.sendStateEventToTest(mLauncher, state.ordinal);
     }
 
-    public void onWindowFocusChanged() {
-        UiFactory.onLauncherStateOrFocusChanged(mLauncher);
-    }
-
     public LauncherState getLastState() {
         return mLastStableState;
     }
diff --git a/src/com/android/launcher3/WorkspaceItemInfo.java b/src/com/android/launcher3/WorkspaceItemInfo.java
index 23795c5..71bd92f 100644
--- a/src/com/android/launcher3/WorkspaceItemInfo.java
+++ b/src/com/android/launcher3/WorkspaceItemInfo.java
@@ -28,7 +28,7 @@
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.uioverrides.UiFactory;
+import com.android.launcher3.uioverrides.ApiWrapper;
 import com.android.launcher3.util.ContentWriter;
 
 import java.util.Arrays;
@@ -192,7 +192,7 @@
         }
         disabledMessage = shortcutInfo.getDisabledMessage();
 
-        Person[] persons = UiFactory.getPersons(shortcutInfo);
+        Person[] persons = ApiWrapper.getPersons(shortcutInfo);
         personKeys = persons.length == 0 ? Utilities.EMPTY_STRING_ARRAY
             : Arrays.stream(persons).map(Person::getKey).sorted().toArray(String[]::new);
     }
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index cdc7061..8823bde 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -57,7 +57,6 @@
 import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.graphics.WorkspaceAndHotseatScrim;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
-import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.launcher3.views.Transposable;
@@ -121,7 +120,7 @@
     }
 
     public void recreateControllers() {
-        mControllers = UiFactory.createTouchControllers(mActivity);
+        mControllers = mActivity.createTouchControllers();
     }
 
     public ViewGroupFocusHelper getFocusIndicatorHelper() {
@@ -477,14 +476,14 @@
     public void onViewAdded(View child) {
         super.onViewAdded(child);
         updateChildIndices();
-        UiFactory.onLauncherStateOrFocusChanged(mActivity);
+        mActivity.onDragLayerHierarchyChanged();
     }
 
     @Override
     public void onViewRemoved(View child) {
         super.onViewRemoved(child);
         updateChildIndices();
-        UiFactory.onLauncherStateOrFocusChanged(mActivity);
+        mActivity.onDragLayerHierarchyChanged();
     }
 
     @Override
diff --git a/src/com/android/launcher3/dragndrop/PinItemDragListener.java b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
index 07eb0d6..869dd94 100644
--- a/src/com/android/launcher3/dragndrop/PinItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
@@ -32,7 +32,6 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.PendingAddItemInfo;
-import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
@@ -68,7 +67,7 @@
     public boolean init(Launcher launcher, boolean alreadyOnHome) {
         super.init(launcher, alreadyOnHome);
         if (!alreadyOnHome) {
-            UiFactory.useFadeOutAnimationForLauncherStart(launcher, mCancelSignal);
+            launcher.useFadeOutAnimationForLauncherStart(mCancelSignal);
         }
         return false;
     }
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
index 227bb22..2d62c9e 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -117,25 +117,30 @@
                     }
                     SessionInfo sessionInfo = packageInstaller.getActiveSessionInfo(item.user,
                             packageName);
+                    List<LauncherActivityInfo> activities = launcherApps
+                            .getActivityList(packageName, item.user);
+                    boolean hasActivity = activities != null && !activities.isEmpty();
+
                     if (sessionInfo == null) {
-                        List<LauncherActivityInfo> activities = launcherApps
-                                .getActivityList(packageName, item.user);
-                        if (activities != null && !activities.isEmpty()) {
-                            // App was installed while launcher was in the background.
-                            itemInfo = new AppInfo(app.getContext(), activities.get(0), item.user)
-                                    .makeWorkspaceItem();
-                            WorkspaceItemInfo wii = (WorkspaceItemInfo) itemInfo;
-                            wii.title = "";
-                            wii.applyFrom(app.getIconCache().getDefaultIcon(item.user));
-                            app.getIconCache().getTitleAndIcon(wii,
-                                    ((WorkspaceItemInfo) itemInfo).usingLowResIcon());
-                        } else {
+                        if (!hasActivity) {
                             // Session was cancelled, do not add.
                             continue;
                         }
                     } else {
                         workspaceInfo.setInstallProgress((int) sessionInfo.getProgress());
                     }
+
+                    if (hasActivity) {
+                        // App was installed while launcher was in the background,
+                        // or app was already installed for another user.
+                        itemInfo = new AppInfo(app.getContext(), activities.get(0), item.user)
+                                .makeWorkspaceItem();
+                        WorkspaceItemInfo wii = (WorkspaceItemInfo) itemInfo;
+                        wii.title = "";
+                        wii.applyFrom(app.getIconCache().getDefaultIcon(item.user));
+                        app.getIconCache().getTitleAndIcon(wii,
+                                ((WorkspaceItemInfo) itemInfo).usingLowResIcon());
+                    }
                 }
 
                 // Add the shortcut to the db
diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java
index 0a4f005..a00a6bd 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -300,8 +300,8 @@
 
     public LooperIdleLock newIdleLock(Object lock) {
         LooperIdleLock idleLock = new LooperIdleLock(lock, Looper.getMainLooper());
-        // If we are not binding, there is no reason to wait for idle.
-        if (mCallbacks.get() == null) {
+        // If we are not binding or if the main looper is already idle, there is no reason to wait
+        if (mCallbacks.get() == null || Looper.getMainLooper().getQueue().isIdle()) {
             idleLock.queueIdle();
         }
         return idleLock;
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 04f15fc..353a0d2 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -164,32 +164,15 @@
     }
 
     public void run() {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE,
-                    "LoaderTask1 " + this);
-        }
         synchronized (this) {
             // Skip fast if we are already stopped.
             if (mStopped) {
                 return;
             }
         }
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE,
-                    "LoaderTask2 " + this);
-        }
 
         Object traceToken = TraceHelper.INSTANCE.beginSection(TAG);
-        TimingLogger logger = TestProtocol.sDebugTracing ?
-                new TimingLogger(TAG, "run") {
-                    @Override
-                    public void addSplit(String splitLabel) {
-                        super.addSplit(splitLabel);
-                        Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE,
-                                "LoaderTask.addSplit " + splitLabel);
-                    }
-                }
-                : new TimingLogger(TAG, "run");
+        TimingLogger logger = new TimingLogger(TAG, "run");
         try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
             loadWorkspace();
             logger.addSplit("loadWorkspace");
@@ -258,10 +241,6 @@
             updateHandler.finish();
             logger.addSplit("finish icon update");
 
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE,
-                        "LoaderTask3 " + this);
-            }
             transaction.commit();
         } catch (CancellationException e) {
             // Loader stopped, ignore
diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java
index 717a7e9..021fb30 100644
--- a/src/com/android/launcher3/notification/NotificationItemView.java
+++ b/src/com/android/launcher3/notification/NotificationItemView.java
@@ -16,7 +16,7 @@
 
 package com.android.launcher3.notification;
 
-import static com.android.launcher3.touch.SwipeDetector.HORIZONTAL;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
 
 import android.app.Notification;
 import android.content.Context;
@@ -30,7 +30,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.popup.PopupContainerWithArrow;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.util.Themes;
 
 import java.util.List;
@@ -49,7 +49,7 @@
     private final TextView mHeaderCount;
     private final NotificationMainView mMainView;
     private final NotificationFooterLayout mFooter;
-    private final SwipeDetector mSwipeDetector;
+    private final SingleAxisSwipeDetector mSwipeDetector;
     private final View mIconView;
 
     private final View mHeader;
@@ -74,8 +74,8 @@
         mHeader = container.findViewById(R.id.header);
         mDivider = container.findViewById(R.id.divider);
 
-        mSwipeDetector = new SwipeDetector(mContext, mMainView, HORIZONTAL);
-        mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, false);
+        mSwipeDetector = new SingleAxisSwipeDetector(mContext, mMainView, HORIZONTAL);
+        mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_BOTH, false);
         mMainView.setSwipeDetector(mSwipeDetector);
         mFooter.setContainer(this);
     }
diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java
index 78627ec..b67adbb 100644
--- a/src/com/android/launcher3/notification/NotificationMainView.java
+++ b/src/com/android/launcher3/notification/NotificationMainView.java
@@ -38,8 +38,9 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.touch.BaseSwipeDetector;
 import com.android.launcher3.touch.OverScroll;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.Themes;
 
@@ -48,7 +49,7 @@
  * e.g. icon + title + text.
  */
 @TargetApi(Build.VERSION_CODES.N)
-public class NotificationMainView extends FrameLayout implements SwipeDetector.Listener {
+public class NotificationMainView extends FrameLayout implements SingleAxisSwipeDetector.Listener {
 
     private static FloatProperty<NotificationMainView> CONTENT_TRANSLATION =
             new FloatProperty<NotificationMainView>("contentTranslation") {
@@ -75,7 +76,7 @@
     private TextView mTextView;
     private View mIconView;
 
-    private SwipeDetector mSwipeDetector;
+    private SingleAxisSwipeDetector mSwipeDetector;
 
     public NotificationMainView(Context context) {
         this(context, null, 0);
@@ -107,7 +108,7 @@
         mIconView = findViewById(R.id.popup_item_icon);
     }
 
-    public void setSwipeDetector(SwipeDetector swipeDetector) {
+    public void setSwipeDetector(SingleAxisSwipeDetector swipeDetector) {
         mSwipeDetector = swipeDetector;
     }
 
@@ -173,7 +174,7 @@
                 LauncherLogProto.ItemType.NOTIFICATION);
     }
 
-    // SwipeDetector.Listener's
+    // SingleAxisSwipeDetector.Listener's
     @Override
     public void onDragStart(boolean start) { }
 
@@ -187,7 +188,7 @@
     }
 
     @Override
-    public void onDragEnd(float velocity, boolean fling) {
+    public void onDragEnd(float velocity) {
         final boolean willExit;
         final float endTranslation;
         final float startTranslation = mTextAndBackground.getTranslationX();
@@ -195,7 +196,7 @@
         if (!canChildBeDismissed()) {
             willExit = false;
             endTranslation = 0;
-        } else if (fling) {
+        } else if (mSwipeDetector.isFling(velocity)) {
             willExit = true;
             endTranslation = velocity < 0 ? - getWidth() : getWidth();
         } else if (Math.abs(startTranslation) > getWidth() / 2) {
@@ -206,7 +207,7 @@
             endTranslation = 0;
         }
 
-        long duration = SwipeDetector.calculateDuration(velocity,
+        long duration = BaseSwipeDetector.calculateDuration(velocity,
                 (endTranslation - startTranslation) / getWidth());
 
         mContentTranslateAnimator.removeAllListeners();
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 4833c26..2034926 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -77,6 +77,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 /**
  * A container for shortcuts to deep links and notifications associated with an app.
@@ -213,7 +214,7 @@
         final PopupContainerWithArrow container =
                 (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
                         R.layout.popup_container, launcher.getDragLayer(), false);
-        container.populateAndShow(icon, itemInfo, SystemShortcutFactory.INSTANCE.get(launcher));
+        container.populateAndShow(icon, itemInfo);
         return container;
     }
 
@@ -238,8 +239,7 @@
         }
     }
 
-    protected void populateAndShow(
-            BubbleTextView icon, ItemInfo item, SystemShortcutFactory factory) {
+    protected void populateAndShow(BubbleTextView icon, ItemInfo item) {
         if (TestProtocol.sDebugTracing) {
             Log.d(TestProtocol.NO_CONTEXT_MENU, "populateAndShow");
         }
@@ -247,7 +247,10 @@
         populateAndShow(icon,
                 popupDataProvider.getShortcutCountForItem(item),
                 popupDataProvider.getNotificationKeysForItem(item),
-                factory.getEnabledShortcuts(mLauncher, item));
+                mLauncher.getSupportedShortcuts()
+                        .map(s -> s.getShortcut(mLauncher, item))
+                        .filter(s -> s != null)
+                        .collect(Collectors.toList()));
     }
 
     public ViewGroup getSystemShortcutContainerForTesting() {
@@ -382,8 +385,7 @@
     @Override
     public void onWidgetsBound() {
         ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
-        SystemShortcut widgetInfo = new SystemShortcut.Widgets();
-        View.OnClickListener onClickListener = widgetInfo.getOnClickListener(mLauncher, itemInfo);
+        SystemShortcut widgetInfo = SystemShortcut.WIDGETS.getShortcut(mLauncher, itemInfo);
         View widgetsView = null;
         int count = mSystemShortcutContainer.getChildCount();
         for (int i = 0; i < count; i++) {
@@ -394,7 +396,7 @@
             }
         }
 
-        if (onClickListener != null && widgetsView == null) {
+        if (widgetInfo != null && widgetsView == null) {
             // We didn't have any widgets cached but now there are some, so enable the shortcut.
             if (mSystemShortcutContainer != this) {
                 initializeSystemShortcut(
@@ -407,7 +409,7 @@
                 close(false);
                 PopupContainerWithArrow.showForIcon(mOriginalIcon);
             }
-        } else if (onClickListener == null && widgetsView != null) {
+        } else if (widgetInfo == null && widgetsView != null) {
             // No widgets exist, but we previously added the shortcut so remove it.
             if (mSystemShortcutContainer != this) {
                 mSystemShortcutContainer.removeView(widgetsView);
@@ -430,8 +432,7 @@
             info.setIconAndContentDescriptionFor((ImageView) view);
         }
         view.setTag(info);
-        view.setOnClickListener(info.getOnClickListener(mLauncher,
-                (ItemInfo) mOriginalIcon.getTag()));
+        view.setOnClickListener(info);
     }
 
     /**
diff --git a/src/com/android/launcher3/popup/RemoteActionShortcut.java b/src/com/android/launcher3/popup/RemoteActionShortcut.java
index 5a5fbab..8751202 100644
--- a/src/com/android/launcher3/popup/RemoteActionShortcut.java
+++ b/src/com/android/launcher3/popup/RemoteActionShortcut.java
@@ -16,13 +16,19 @@
 
 package com.android.launcher3.popup;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.annotation.TargetApi;
 import android.app.PendingIntent;
 import android.app.RemoteAction;
+import android.content.Context;
 import android.content.Intent;
-import android.os.Handler;
-import android.os.Looper;
+import android.os.Build;
 import android.util.Log;
 import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.ImageView;
+import android.widget.TextView;
 import android.widget.Toast;
 
 import com.android.launcher3.AbstractFloatingView;
@@ -32,55 +38,75 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 
+@TargetApi(Build.VERSION_CODES.Q)
 public class RemoteActionShortcut extends SystemShortcut<BaseDraggingActivity> {
     private static final String TAG = "RemoteActionShortcut";
     private static final boolean DEBUG = Utilities.IS_DEBUG_DEVICE;
 
     private final RemoteAction mAction;
 
-    public RemoteActionShortcut(RemoteAction action) {
-        super(action.getIcon(), action.getTitle(), action.getContentDescription(),
-                R.id.action_remote_action_shortcut);
+    public RemoteActionShortcut(RemoteAction action,
+            BaseDraggingActivity activity, ItemInfo itemInfo) {
+        super(0, R.id.action_remote_action_shortcut, activity, itemInfo);
         mAction = action;
     }
 
     @Override
-    public View.OnClickListener getOnClickListener(
-            final BaseDraggingActivity activity, final ItemInfo itemInfo) {
-        return view -> {
-            AbstractFloatingView.closeAllOpenViews(activity);
+    public void setIconAndLabelFor(View iconView, TextView labelView) {
+        mAction.getIcon().loadDrawableAsync(iconView.getContext(),
+                iconView::setBackground,
+                MAIN_EXECUTOR.getHandler());
+        labelView.setText(mAction.getTitle());
+    }
 
-            final String actionIdentity = mAction.getTitle() + ", " +
-                    itemInfo.getTargetComponent().getPackageName();
-            try {
-                if (DEBUG) Log.d(TAG, "Sending action: " + actionIdentity);
-                mAction.getActionIntent().send(
-                        activity,
-                        0,
-                        new Intent().putExtra(
-                                Intent.EXTRA_PACKAGE_NAME,
-                                itemInfo.getTargetComponent().getPackageName()),
-                        (pendingIntent, intent, resultCode, resultData, resultExtras) -> {
-                            if (DEBUG) Log.d(TAG, "Action is complete: " + actionIdentity);
-                            if (resultData != null && !resultData.isEmpty()) {
-                                Log.e(TAG, "Remote action returned result: " + actionIdentity
-                                        + " : " + resultData);
-                                Toast.makeText(activity, resultData, Toast.LENGTH_SHORT).show();
-                            }
-                        },
-                        new Handler(Looper.getMainLooper()));
-            } catch (PendingIntent.CanceledException e) {
-                Log.e(TAG, "Remote action canceled: " + actionIdentity, e);
-                Toast.makeText(activity, activity.getString(
-                        R.string.remote_action_failed,
-                        mAction.getTitle()),
-                        Toast.LENGTH_SHORT)
-                        .show();
-            }
+    @Override
+    public void setIconAndContentDescriptionFor(ImageView view) {
+        mAction.getIcon().loadDrawableAsync(view.getContext(),
+                view::setImageDrawable,
+                MAIN_EXECUTOR.getHandler());
+        view.setContentDescription(mAction.getContentDescription());
+    }
 
-            activity.getUserEventDispatcher().logActionOnControl(LauncherLogProto.Action.Touch.TAP,
-                    LauncherLogProto.ControlType.REMOTE_ACTION_SHORTCUT, view);
-        };
+    @Override
+    public AccessibilityNodeInfo.AccessibilityAction createAccessibilityAction(Context context) {
+        return new AccessibilityNodeInfo.AccessibilityAction(
+                R.id.action_remote_action_shortcut, mAction.getContentDescription());
+    }
+
+    @Override
+    public void onClick(View view) {
+        AbstractFloatingView.closeAllOpenViews(mTarget);
+
+        final String actionIdentity = mAction.getTitle() + ", "
+                + mItemInfo.getTargetComponent().getPackageName();
+        try {
+            if (DEBUG) Log.d(TAG, "Sending action: " + actionIdentity);
+            mAction.getActionIntent().send(
+                    mTarget,
+                    0,
+                    new Intent().putExtra(
+                            Intent.EXTRA_PACKAGE_NAME,
+                            mItemInfo.getTargetComponent().getPackageName()),
+                    (pendingIntent, intent, resultCode, resultData, resultExtras) -> {
+                        if (DEBUG) Log.d(TAG, "Action is complete: " + actionIdentity);
+                        if (resultData != null && !resultData.isEmpty()) {
+                            Log.e(TAG, "Remote action returned result: " + actionIdentity
+                                    + " : " + resultData);
+                            Toast.makeText(mTarget, resultData, Toast.LENGTH_SHORT).show();
+                        }
+                    },
+                    MAIN_EXECUTOR.getHandler());
+        } catch (PendingIntent.CanceledException e) {
+            Log.e(TAG, "Remote action canceled: " + actionIdentity, e);
+            Toast.makeText(mTarget, mTarget.getString(
+                    R.string.remote_action_failed,
+                    mAction.getTitle()),
+                    Toast.LENGTH_SHORT)
+                    .show();
+        }
+
+        mTarget.getUserEventDispatcher().logActionOnControl(LauncherLogProto.Action.Touch.TAP,
+                LauncherLogProto.ControlType.REMOTE_ACTION_SHORTCUT, view);
     }
 
     @Override
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index a87b7b8..222c6c9 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -5,14 +5,13 @@
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
-import android.graphics.drawable.Icon;
-import android.os.Handler;
-import android.os.Looper;
 import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.ImageView;
 import android.widget.TextView;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.ItemInfo;
@@ -39,41 +38,30 @@
  * Example system shortcuts, defined as inner classes, include Widgets and AppInfo.
  * @param <T>
  */
-public abstract class SystemShortcut<T extends BaseDraggingActivity>
-        extends ItemInfo {
+public abstract class SystemShortcut<T extends BaseDraggingActivity> extends ItemInfo
+        implements View.OnClickListener {
+
     private final int mIconResId;
     private final int mLabelResId;
-    private final Icon mIcon;
-    private final CharSequence mLabel;
-    private final CharSequence mContentDescription;
     private final int mAccessibilityActionId;
 
-    public SystemShortcut(int iconResId, int labelResId) {
+    protected final T mTarget;
+    protected final ItemInfo mItemInfo;
+
+    public SystemShortcut(int iconResId, int labelResId, T target, ItemInfo itemInfo) {
         mIconResId = iconResId;
         mLabelResId = labelResId;
         mAccessibilityActionId = labelResId;
-        mIcon = null;
-        mLabel = null;
-        mContentDescription = null;
+        mTarget = target;
+        mItemInfo = itemInfo;
     }
 
-    public SystemShortcut(Icon icon, CharSequence label, CharSequence contentDescription,
-            int accessibilityActionId) {
-        mIcon = icon;
-        mLabel = label;
-        mContentDescription = contentDescription;
-        mAccessibilityActionId = accessibilityActionId;
-        mIconResId = 0;
-        mLabelResId = 0;
-    }
-
-    public SystemShortcut(SystemShortcut other) {
+    public SystemShortcut(SystemShortcut<T> other) {
         mIconResId = other.mIconResId;
         mLabelResId = other.mLabelResId;
-        mIcon = other.mIcon;
-        mLabel = other.mLabel;
-        mContentDescription = other.mContentDescription;
         mAccessibilityActionId = other.mAccessibilityActionId;
+        mTarget = other.mTarget;
+        mItemInfo = other.mItemInfo;
     }
 
     /**
@@ -84,150 +72,135 @@
     }
 
     public void setIconAndLabelFor(View iconView, TextView labelView) {
-        if (mIcon != null) {
-            mIcon.loadDrawableAsync(iconView.getContext(),
-                    iconView::setBackground,
-                    new Handler(Looper.getMainLooper()));
-        } else {
-            iconView.setBackgroundResource(mIconResId);
-        }
-
-        if (mLabel != null) {
-            labelView.setText(mLabel);
-        } else {
-            labelView.setText(mLabelResId);
-        }
+        iconView.setBackgroundResource(mIconResId);
+        labelView.setText(mLabelResId);
     }
 
     public void setIconAndContentDescriptionFor(ImageView view) {
-        if (mIcon != null) {
-            mIcon.loadDrawableAsync(view.getContext(),
-                    view::setImageDrawable,
-                    new Handler(Looper.getMainLooper()));
-        } else {
-            view.setImageResource(mIconResId);
-        }
-
-        view.setContentDescription(getContentDescription(view.getContext()));
-    }
-
-    private CharSequence getContentDescription(Context context) {
-        return mContentDescription != null ? mContentDescription : context.getText(mLabelResId);
+        view.setImageResource(mIconResId);
+        view.setContentDescription(view.getContext().getText(mLabelResId));
     }
 
     public AccessibilityNodeInfo.AccessibilityAction createAccessibilityAction(Context context) {
-        return new AccessibilityNodeInfo.AccessibilityAction(mAccessibilityActionId,
-                getContentDescription(context));
+        return new AccessibilityNodeInfo.AccessibilityAction(
+                mAccessibilityActionId, context.getText(mLabelResId));
     }
 
     public boolean hasHandlerForAction(int action) {
         return mAccessibilityActionId == action;
     }
 
-    public abstract View.OnClickListener getOnClickListener(T activity, ItemInfo itemInfo);
+    public interface Factory<T extends BaseDraggingActivity> {
+
+        @Nullable SystemShortcut<T> getShortcut(T activity, ItemInfo itemInfo);
+    }
+
+    public static final Factory<Launcher> WIDGETS = (launcher, itemInfo) -> {
+        if (itemInfo.getTargetComponent() == null) return null;
+        final List<WidgetItem> widgets =
+                launcher.getPopupDataProvider().getWidgetsForPackageUser(new PackageUserKey(
+                        itemInfo.getTargetComponent().getPackageName(), itemInfo.user));
+        if (widgets == null) {
+            return null;
+        }
+        return new Widgets(launcher, itemInfo);
+    };
 
     public static class Widgets extends SystemShortcut<Launcher> {
 
-        public Widgets() {
-            super(R.drawable.ic_widget, R.string.widget_button_text);
+        public Widgets(Launcher target, ItemInfo itemInfo) {
+            super(R.drawable.ic_widget, R.string.widget_button_text, target, itemInfo);
         }
 
         @Override
-        public View.OnClickListener getOnClickListener(final Launcher launcher,
-                final ItemInfo itemInfo) {
-            if (itemInfo.getTargetComponent() == null) return null;
-            final List<WidgetItem> widgets =
-                    launcher.getPopupDataProvider().getWidgetsForPackageUser(new PackageUserKey(
-                            itemInfo.getTargetComponent().getPackageName(), itemInfo.user));
-            if (widgets == null) {
-                return null;
-            }
-            return (view) -> {
-                AbstractFloatingView.closeAllOpenViews(launcher);
-                WidgetsBottomSheet widgetsBottomSheet =
-                        (WidgetsBottomSheet) launcher.getLayoutInflater().inflate(
-                                R.layout.widgets_bottom_sheet, launcher.getDragLayer(), false);
-                widgetsBottomSheet.populateAndShow(itemInfo);
-                launcher.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
-                        ControlType.WIDGETS_BUTTON, view);
-            };
+        public void onClick(View view) {
+            AbstractFloatingView.closeAllOpenViews(mTarget);
+            WidgetsBottomSheet widgetsBottomSheet =
+                    (WidgetsBottomSheet) mTarget.getLayoutInflater().inflate(
+                            R.layout.widgets_bottom_sheet, mTarget.getDragLayer(), false);
+            widgetsBottomSheet.populateAndShow(mItemInfo);
+            mTarget.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
+                    ControlType.WIDGETS_BUTTON, view);
         }
     }
 
+    public static final Factory<BaseDraggingActivity> APP_INFO = AppInfo::new;
+
     public static class AppInfo extends SystemShortcut {
-        public AppInfo() {
-            super(R.drawable.ic_info_no_shadow, R.string.app_info_drop_target_label);
+
+        public AppInfo(BaseDraggingActivity target, ItemInfo itemInfo) {
+            super(R.drawable.ic_info_no_shadow, R.string.app_info_drop_target_label, target,
+                    itemInfo);
         }
 
         @Override
-        public View.OnClickListener getOnClickListener(
-                BaseDraggingActivity activity, ItemInfo itemInfo) {
-            return (view) -> {
-                dismissTaskMenuView(activity);
-                Rect sourceBounds = activity.getViewBounds(view);
-                new PackageManagerHelper(activity).startDetailsActivityForInfo(
-                        itemInfo, sourceBounds, ActivityOptions.makeBasic().toBundle());
-                activity.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
-                        ControlType.APPINFO_TARGET, view);
-            };
+        public void onClick(View view) {
+            dismissTaskMenuView(mTarget);
+            Rect sourceBounds = mTarget.getViewBounds(view);
+            new PackageManagerHelper(mTarget).startDetailsActivityForInfo(
+                    mItemInfo, sourceBounds, ActivityOptions.makeBasic().toBundle());
+            mTarget.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
+                    ControlType.APPINFO_TARGET, view);
         }
     }
 
+    public static Factory<BaseDraggingActivity> INSTALL = (activity, itemInfo) -> {
+        boolean supportsWebUI = (itemInfo instanceof WorkspaceItemInfo)
+                && ((WorkspaceItemInfo) itemInfo).hasStatusFlag(
+                        WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI);
+        boolean isInstantApp = false;
+        if (itemInfo instanceof com.android.launcher3.AppInfo) {
+            com.android.launcher3.AppInfo appInfo = (com.android.launcher3.AppInfo) itemInfo;
+            isInstantApp = InstantAppResolver.newInstance(activity).isInstantApp(appInfo);
+        }
+        boolean enabled = supportsWebUI || isInstantApp;
+        if (!enabled) {
+            return null;
+        }
+        return new Install(activity, itemInfo);
+    };
+
     public static class Install extends SystemShortcut {
-        public Install() {
-            super(R.drawable.ic_install_no_shadow, R.string.install_drop_target_label);
+
+        public Install(BaseDraggingActivity target, ItemInfo itemInfo) {
+            super(R.drawable.ic_install_no_shadow, R.string.install_drop_target_label,
+                    target, itemInfo);
         }
 
         @Override
-        public View.OnClickListener getOnClickListener(
-                BaseDraggingActivity activity, ItemInfo itemInfo) {
-            boolean supportsWebUI = (itemInfo instanceof WorkspaceItemInfo) &&
-                    ((WorkspaceItemInfo) itemInfo).hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI);
-            boolean isInstantApp = false;
-            if (itemInfo instanceof com.android.launcher3.AppInfo) {
-                com.android.launcher3.AppInfo appInfo = (com.android.launcher3.AppInfo) itemInfo;
-                isInstantApp = InstantAppResolver.newInstance(activity).isInstantApp(appInfo);
-            }
-            boolean enabled = supportsWebUI || isInstantApp;
-            if (!enabled) {
-                return null;
-            }
-            return createOnClickListener(activity, itemInfo);
-        }
-
-        public View.OnClickListener createOnClickListener(
-                BaseDraggingActivity activity, ItemInfo itemInfo) {
-            return view -> {
-                Intent intent = new PackageManagerHelper(view.getContext()).getMarketIntent(
-                        itemInfo.getTargetComponent().getPackageName());
-                activity.startActivitySafely(view, intent, itemInfo, null);
-                AbstractFloatingView.closeAllOpenViews(activity);
-            };
+        public void onClick(View view) {
+            Intent intent = new PackageManagerHelper(view.getContext()).getMarketIntent(
+                    mItemInfo.getTargetComponent().getPackageName());
+            mTarget.startActivitySafely(view, intent, mItemInfo, null);
+            AbstractFloatingView.closeAllOpenViews(mTarget);
         }
     }
 
+    public static Factory<Launcher> DISMISS_PREDICTION = (launcher, itemInfo) -> {
+        if (!FeatureFlags.ENABLE_PREDICTION_DISMISS.get()) return null;
+        if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_PREDICTION) return null;
+        return new DismissPrediction(launcher, itemInfo);
+    };
+
     public static class DismissPrediction extends SystemShortcut<Launcher> {
-        public DismissPrediction() {
-            super(R.drawable.ic_remove_no_shadow, R.string.dismiss_prediction_label);
+        public DismissPrediction(Launcher launcher, ItemInfo itemInfo) {
+            super(R.drawable.ic_remove_no_shadow, R.string.dismiss_prediction_label, launcher,
+                    itemInfo);
         }
 
         @Override
-        public View.OnClickListener getOnClickListener(Launcher activity, ItemInfo itemInfo) {
-            if (!FeatureFlags.ENABLE_PREDICTION_DISMISS.get()) return null;
-            if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_PREDICTION) return null;
-            return (view) -> {
-                PopupContainerWithArrow.closeAllOpenViews(activity);
-                activity.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
-                        ControlType.DISMISS_PREDICTION, ContainerType.DEEPSHORTCUTS);
-                AppLaunchTracker.INSTANCE.get(view.getContext())
-                        .onDismissApp(itemInfo.getTargetComponent(),
-                                itemInfo.user,
-                                AppLaunchTracker.CONTAINER_PREDICTIONS);
-            };
+        public void onClick(View view) {
+            PopupContainerWithArrow.closeAllOpenViews(mTarget);
+            mTarget.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
+                    ControlType.DISMISS_PREDICTION, ContainerType.DEEPSHORTCUTS);
+            AppLaunchTracker.INSTANCE.get(view.getContext()).onDismissApp(
+                    mItemInfo.getTargetComponent(),
+                    mItemInfo.user,
+                    AppLaunchTracker.CONTAINER_PREDICTIONS);
         }
     }
 
-    protected static void dismissTaskMenuView(BaseDraggingActivity activity) {
+    public static void dismissTaskMenuView(BaseDraggingActivity activity) {
         AbstractFloatingView.closeOpenViews(activity, true,
             AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
     }
diff --git a/src/com/android/launcher3/popup/SystemShortcutFactory.java b/src/com/android/launcher3/popup/SystemShortcutFactory.java
deleted file mode 100644
index dfcc2f8..0000000
--- a/src/com/android/launcher3/popup/SystemShortcutFactory.java
+++ /dev/null
@@ -1,61 +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.popup;
-
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class SystemShortcutFactory implements ResourceBasedOverride {
-
-    public static final MainThreadInitializedObject<SystemShortcutFactory> INSTANCE =
-            forOverride(SystemShortcutFactory.class, R.string.system_shortcut_factory_class);
-
-    /** Note that these are in order of priority. */
-    private final SystemShortcut[] mAllShortcuts;
-
-    @SuppressWarnings("unused")
-    public SystemShortcutFactory() {
-        this(new SystemShortcut.AppInfo(),
-                new SystemShortcut.Widgets(),
-                new SystemShortcut.Install(),
-                new SystemShortcut.DismissPrediction());
-    }
-
-    protected SystemShortcutFactory(SystemShortcut... shortcuts) {
-        mAllShortcuts = shortcuts;
-    }
-
-    public @NonNull List<SystemShortcut> getEnabledShortcuts(Launcher launcher, ItemInfo info) {
-        List<SystemShortcut> systemShortcuts = new ArrayList<>();
-        for (SystemShortcut systemShortcut : mAllShortcuts) {
-            if (systemShortcut.getOnClickListener(launcher, info) != null) {
-                systemShortcuts.add(systemShortcut);
-            }
-        }
-
-        return systemShortcuts;
-    }
-}
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index d0e648f..140a06a 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -177,11 +177,6 @@
     }
 
     protected boolean isLauncherInitialized() {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE,
-                    "isLauncherInitialized " + Launcher.ACTIVITY_TRACKER.getCreatedActivity() + ", "
-                            + LauncherAppState.getInstance(mContext).getModel().isModelLoaded());
-        }
         return Launcher.ACTIVITY_TRACKER.getCreatedActivity() == null
                 || LauncherAppState.getInstance(mContext).getModel().isModelLoaded();
     }
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 923c466..1766814 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -85,6 +85,5 @@
     public static final String NO_DRAG_TO_WORKSPACE = "b/138729456";
     public static final String APP_NOT_DISABLED = "b/139891609";
     public static final String NO_CONTEXT_MENU = "b/141770616";
-    public static final String LAUNCHER_DIDNT_INITIALIZE = "b/142514365";
     public static final String CRASH_ADD_CUSTOM_SHORTCUT = "b/141568904";
 }
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index c5ba5ba..60f6ee9 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -53,7 +53,7 @@
  * TouchController for handling state changes
  */
 public abstract class AbstractStateChangeTouchController
-        implements TouchController, SwipeDetector.Listener {
+        implements TouchController, SingleAxisSwipeDetector.Listener {
 
     // 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;
@@ -65,8 +65,8 @@
     protected final long ATOMIC_DURATION = getAtomicDuration();
 
     protected final Launcher mLauncher;
-    protected final SwipeDetector mDetector;
-    protected final SwipeDetector.Direction mSwipeDirection;
+    protected final SingleAxisSwipeDetector mDetector;
+    protected final SingleAxisSwipeDetector.Direction mSwipeDirection;
 
     private boolean mNoIntercept;
     private boolean mIsLogContainerSet;
@@ -101,9 +101,9 @@
 
     private float mAtomicComponentsStartProgress;
 
-    public AbstractStateChangeTouchController(Launcher l, SwipeDetector.Direction dir) {
+    public AbstractStateChangeTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) {
         mLauncher = l;
-        mDetector = new SwipeDetector(l, this, dir);
+        mDetector = new SingleAxisSwipeDetector(l, this, dir);
         mSwipeDirection = dir;
     }
 
@@ -127,7 +127,7 @@
             boolean ignoreSlopWhenSettling = false;
 
             if (mCurrentAnimation != null) {
-                directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
+                directionsToDetectScroll = SingleAxisSwipeDetector.DIRECTION_BOTH;
                 ignoreSlopWhenSettling = true;
             } else {
                 directionsToDetectScroll = getSwipeDirection();
@@ -152,10 +152,10 @@
         LauncherState fromState = mLauncher.getStateManager().getState();
         int swipeDirection = 0;
         if (getTargetState(fromState, true /* isDragTowardPositive */) != fromState) {
-            swipeDirection |= SwipeDetector.DIRECTION_POSITIVE;
+            swipeDirection |= SingleAxisSwipeDetector.DIRECTION_POSITIVE;
         }
         if (getTargetState(fromState, false /* isDragTowardPositive */) != fromState) {
-            swipeDirection |= SwipeDetector.DIRECTION_NEGATIVE;
+            swipeDirection |= SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
         }
         return swipeDirection;
     }
@@ -369,7 +369,8 @@
     }
 
     @Override
-    public void onDragEnd(float velocity, boolean fling) {
+    public void onDragEnd(float velocity) {
+        boolean fling = mDetector.isFling(velocity);
         final int logAction = fling ? Touch.FLING : Touch.SWIPE;
 
         boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
@@ -406,7 +407,7 @@
             } else {
                 startProgress = Utilities.boundToRange(progress
                         + velocity * getSingleFrameMs(mLauncher) * mProgressMultiplier, 0f, 1f);
-                duration = SwipeDetector.calculateDuration(velocity,
+                duration = BaseSwipeDetector.calculateDuration(velocity,
                         endProgress - Math.max(progress, 0)) * durationMultiplier;
             }
         } else {
@@ -424,7 +425,7 @@
             } else {
                 startProgress = Utilities.boundToRange(progress
                         + velocity * getSingleFrameMs(mLauncher) * mProgressMultiplier, 0f, 1f);
-                duration = SwipeDetector.calculateDuration(velocity,
+                duration = BaseSwipeDetector.calculateDuration(velocity,
                         Math.min(progress, 1) - endProgress) * durationMultiplier;
             }
         }
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java b/src/com/android/launcher3/touch/AllAppsSwipeController.java
similarity index 75%
rename from src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
rename to src/com/android/launcher3/touch/AllAppsSwipeController.java
index bd6ea50..31a5d79 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
+++ b/src/com/android/launcher3/touch/AllAppsSwipeController.java
@@ -1,4 +1,19 @@
-package com.android.launcher3.uioverrides;
+/**
+ * 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.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
@@ -9,8 +24,6 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager.AnimationComponents;
-import com.android.launcher3.touch.AbstractStateChangeTouchController;
-import com.android.launcher3.touch.SwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 
 /**
@@ -21,7 +34,7 @@
     private MotionEvent mTouchDownEvent;
 
     public AllAppsSwipeController(Launcher l) {
-        super(l, SwipeDetector.VERTICAL);
+        super(l, SingleAxisSwipeDetector.VERTICAL);
     }
 
     @Override
@@ -58,8 +71,8 @@
 
     @Override
     protected int getLogContainerTypeForNormalState(MotionEvent ev) {
-        return mLauncher.getDragLayer().isEventOverView(mLauncher.getHotseat(), mTouchDownEvent) ?
-                ContainerType.HOTSEAT : ContainerType.WORKSPACE;
+        return mLauncher.getDragLayer().isEventOverView(mLauncher.getHotseat(), mTouchDownEvent)
+                ? ContainerType.HOTSEAT : ContainerType.WORKSPACE;
     }
 
     @Override
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
new file mode 100644
index 0000000..08d73d0
--- /dev/null
+++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java
@@ -0,0 +1,267 @@
+/*
+ * 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.touch;
+
+import static android.view.MotionEvent.INVALID_POINTER_ID;
+
+import android.graphics.PointF;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Scroll/drag/swipe gesture detector.
+ *
+ * Definition of swipe is different from android system in that this detector handles
+ * 'swipe to dismiss', 'swiping up/down a container' but also keeps scrolling state before
+ * swipe action happens.
+ *
+ * @see SingleAxisSwipeDetector
+ */
+public abstract class BaseSwipeDetector {
+
+    private static final boolean DBG = false;
+    private static final String TAG = "BaseSwipeDetector";
+    private static final float ANIMATION_DURATION = 1200;
+    /** The minimum release velocity in pixels per millisecond that triggers fling.*/
+    private static final float RELEASE_VELOCITY_PX_MS = 1.0f;
+    private static final PointF sTempPoint = new PointF();
+
+    private final PointF mDownPos = new PointF();
+    private final PointF mLastPos = new PointF();
+    protected final boolean mIsRtl;
+    protected final float mTouchSlop;
+    protected final float mMaxVelocity;
+
+    private int mActivePointerId = INVALID_POINTER_ID;
+    private VelocityTracker mVelocityTracker;
+    private PointF mLastDisplacement = new PointF();
+    private PointF mDisplacement = new PointF();
+    protected PointF mSubtractDisplacement = new PointF();
+    private ScrollState mState = ScrollState.IDLE;
+
+    protected boolean mIgnoreSlopWhenSettling;
+
+    private enum ScrollState {
+        IDLE,
+        DRAGGING,      // onDragStart, onDrag
+        SETTLING       // onDragEnd
+    }
+
+    protected BaseSwipeDetector(@NonNull ViewConfiguration config, boolean isRtl) {
+        mTouchSlop = config.getScaledTouchSlop();
+        mMaxVelocity = config.getScaledMaximumFlingVelocity();
+        mIsRtl = isRtl;
+    }
+
+    public static long calculateDuration(float velocity, float progressNeeded) {
+        // TODO: make these values constants after tuning.
+        float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity));
+        float travelDistance = Math.max(0.2f, progressNeeded);
+        long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
+        if (DBG) {
+            Log.d(TAG, String.format(
+                    "calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded));
+        }
+        return duration;
+    }
+
+    public int getDownX() {
+        return (int) mDownPos.x;
+    }
+
+    public int getDownY() {
+        return (int) mDownPos.y;
+    }
+    /**
+     * There's no touch and there's no animation.
+     */
+    public boolean isIdleState() {
+        return mState == ScrollState.IDLE;
+    }
+
+    public boolean isSettlingState() {
+        return mState == ScrollState.SETTLING;
+    }
+
+    public boolean isDraggingState() {
+        return mState == ScrollState.DRAGGING;
+    }
+
+    public boolean isDraggingOrSettling() {
+        return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
+    }
+
+    public void finishedScrolling() {
+        setState(ScrollState.IDLE);
+    }
+
+    public boolean isFling(float velocity) {
+        return Math.abs(velocity) > RELEASE_VELOCITY_PX_MS;
+    }
+
+    public boolean onTouchEvent(MotionEvent ev) {
+        int actionMasked = ev.getActionMasked();
+        if (actionMasked == MotionEvent.ACTION_DOWN && mVelocityTracker != null) {
+            mVelocityTracker.clear();
+        }
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(ev);
+
+        switch (actionMasked) {
+            case MotionEvent.ACTION_DOWN:
+                mActivePointerId = ev.getPointerId(0);
+                mDownPos.set(ev.getX(), ev.getY());
+                mLastPos.set(mDownPos);
+                mLastDisplacement.set(0, 0);
+                mDisplacement.set(0, 0);
+
+                if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
+                    setState(ScrollState.DRAGGING);
+                }
+                break;
+            //case MotionEvent.ACTION_POINTER_DOWN:
+            case MotionEvent.ACTION_POINTER_UP:
+                int ptrIdx = ev.getActionIndex();
+                int ptrId = ev.getPointerId(ptrIdx);
+                if (ptrId == mActivePointerId) {
+                    final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
+                    mDownPos.set(
+                            ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
+                            ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
+                    mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
+                    mActivePointerId = ev.getPointerId(newPointerIdx);
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                if (pointerIndex == INVALID_POINTER_ID) {
+                    break;
+                }
+                mDisplacement.set(ev.getX(pointerIndex) - mDownPos.x,
+                        ev.getY(pointerIndex) - mDownPos.y);
+                if (mIsRtl) {
+                    mDisplacement.x = -mDisplacement.x;
+                }
+
+                // handle state and listener calls.
+                if (mState != ScrollState.DRAGGING && shouldScrollStart(mDisplacement)) {
+                    setState(ScrollState.DRAGGING);
+                }
+                if (mState == ScrollState.DRAGGING) {
+                    reportDragging(ev);
+                }
+                mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+                break;
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                // These are synthetic events and there is no need to update internal values.
+                if (mState == ScrollState.DRAGGING) {
+                    setState(ScrollState.SETTLING);
+                }
+                mVelocityTracker.recycle();
+                mVelocityTracker = null;
+                break;
+            default:
+                break;
+        }
+        return true;
+    }
+
+    //------------------- ScrollState transition diagram -----------------------------------
+    //
+    // IDLE -> (mDisplacement > mTouchSlop) -> DRAGGING
+    // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING
+    // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING
+    // SETTLING -> (View settled) -> IDLE
+
+    private void setState(ScrollState newState) {
+        if (DBG) {
+            Log.d(TAG, "setState:" + mState + "->" + newState);
+        }
+        // onDragStart and onDragEnd is reported ONLY on state transition
+        if (newState == ScrollState.DRAGGING) {
+            initializeDragging();
+            if (mState == ScrollState.IDLE) {
+                reportDragStart(false /* recatch */);
+            } else if (mState == ScrollState.SETTLING) {
+                reportDragStart(true /* recatch */);
+            }
+        }
+        if (newState == ScrollState.SETTLING) {
+            reportDragEnd();
+        }
+
+        mState = newState;
+    }
+
+    private void initializeDragging() {
+        if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
+            mSubtractDisplacement.set(0, 0);
+        } else {
+            mSubtractDisplacement.x = mDisplacement.x > 0 ? mTouchSlop : -mTouchSlop;
+            mSubtractDisplacement.y = mDisplacement.y > 0 ? mTouchSlop : -mTouchSlop;
+        }
+    }
+
+    protected abstract boolean shouldScrollStart(PointF displacement);
+
+    private void reportDragStart(boolean recatch) {
+        reportDragStartInternal(recatch);
+        if (DBG) {
+            Log.d(TAG, "onDragStart recatch:" + recatch);
+        }
+    }
+
+    protected abstract void reportDragStartInternal(boolean recatch);
+
+    private void reportDragging(MotionEvent event) {
+        if (mDisplacement != mLastDisplacement) {
+            if (DBG) {
+                Log.d(TAG, String.format("onDrag disp=%s", mDisplacement));
+            }
+
+            mLastDisplacement.set(mDisplacement);
+            sTempPoint.set(mDisplacement.x - mSubtractDisplacement.x,
+                    mDisplacement.y - mSubtractDisplacement.y);
+            reportDraggingInternal(sTempPoint, event);
+        }
+    }
+
+    protected abstract void reportDraggingInternal(PointF displacement, MotionEvent event);
+
+    private void reportDragEnd() {
+        mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+        PointF velocity = new PointF(mVelocityTracker.getXVelocity() / 1000,
+                mVelocityTracker.getYVelocity() / 1000);
+        if (mIsRtl) {
+            velocity.x = -velocity.x;
+        }
+        if (DBG) {
+            Log.d(TAG, String.format("onScrollEnd disp=%.1s, velocity=%.1s",
+                    mDisplacement, velocity));
+        }
+
+        reportDragEndInternal(velocity);
+    }
+
+    protected abstract void reportDragEndInternal(PointF velocity);
+}
diff --git a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
new file mode 100644
index 0000000..0bf2ff6
--- /dev/null
+++ b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
@@ -0,0 +1,189 @@
+/*
+ * 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 android.content.Context;
+import android.graphics.PointF;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.Utilities;
+
+/**
+ * One dimensional scroll/drag/swipe gesture detector (either HORIZONTAL or VERTICAL).
+ */
+public class SingleAxisSwipeDetector extends BaseSwipeDetector {
+
+    public static final int DIRECTION_POSITIVE = 1 << 0;
+    public static final int DIRECTION_NEGATIVE = 1 << 1;
+    public static final int DIRECTION_BOTH = DIRECTION_NEGATIVE | DIRECTION_POSITIVE;
+
+    public static final Direction VERTICAL = new Direction() {
+
+        @Override
+        boolean isPositive(float displacement) {
+            // Up
+            return displacement < 0;
+        }
+
+        @Override
+        boolean isNegative(float displacement) {
+            // Down
+            return displacement > 0;
+        }
+
+        @Override
+        float extractDirection(PointF direction) {
+            return direction.y;
+        }
+
+        @Override
+        boolean canScrollStart(PointF displacement, float touchSlop) {
+            return Math.abs(displacement.y) >= Math.max(Math.abs(displacement.x), touchSlop);
+        }
+
+    };
+
+    public static final Direction HORIZONTAL = new Direction() {
+
+        @Override
+        boolean isPositive(float displacement) {
+            // Right
+            return displacement > 0;
+        }
+
+        @Override
+        boolean isNegative(float displacement) {
+            // Left
+            return displacement < 0;
+        }
+
+        @Override
+        float extractDirection(PointF direction) {
+            return direction.x;
+        }
+
+        @Override
+        boolean canScrollStart(PointF displacement, float touchSlop) {
+            return Math.abs(displacement.x) >= Math.max(Math.abs(displacement.y), touchSlop);
+        }
+    };
+
+    private final Direction mDir;
+    /* Client of this gesture detector can register a callback. */
+    private final Listener mListener;
+
+    private int mScrollDirections;
+
+    public SingleAxisSwipeDetector(@NonNull Context context, @NonNull Listener l,
+            @NonNull Direction dir) {
+        this(ViewConfiguration.get(context), l, dir, Utilities.isRtl(context.getResources()));
+    }
+
+    @VisibleForTesting
+    protected SingleAxisSwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
+            @NonNull Direction dir, boolean isRtl) {
+        super(config, isRtl);
+        mListener = l;
+        mDir = dir;
+    }
+
+    public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
+        mScrollDirections = scrollDirectionFlags;
+        mIgnoreSlopWhenSettling = ignoreSlop;
+    }
+
+    public int getScrollDirections() {
+        return mScrollDirections;
+    }
+
+    /**
+     * Returns if the start drag was towards the positive direction or negative.
+     *
+     * @see #setDetectableScrollConditions(int, boolean)
+     * @see #DIRECTION_BOTH
+     */
+    public boolean wasInitialTouchPositive() {
+        return mDir.isPositive(mDir.extractDirection(mSubtractDisplacement));
+    }
+
+    @Override
+    protected boolean shouldScrollStart(PointF displacement) {
+        // Reject cases where the angle or slop condition is not met.
+        if (!mDir.canScrollStart(displacement, mTouchSlop)) {
+            return false;
+        }
+
+        // Check if the client is interested in scroll in current direction.
+        float displacementComponent = mDir.extractDirection(displacement);
+        return canScrollNegative(displacementComponent) || canScrollPositive(displacementComponent);
+    }
+
+    private boolean canScrollNegative(float displacement) {
+        return (mScrollDirections & DIRECTION_NEGATIVE) > 0 && mDir.isNegative(displacement);
+    }
+
+    private boolean canScrollPositive(float displacement) {
+        return (mScrollDirections & DIRECTION_POSITIVE) > 0 && mDir.isPositive(displacement);
+    }
+
+    @Override
+    protected void reportDragStartInternal(boolean recatch) {
+        mListener.onDragStart(!recatch);
+    }
+
+    @Override
+    protected void reportDraggingInternal(PointF displacement, MotionEvent event) {
+        mListener.onDrag(mDir.extractDirection(displacement), event);
+    }
+
+    @Override
+    protected void reportDragEndInternal(PointF velocity) {
+        float velocityComponent = mDir.extractDirection(velocity);
+        mListener.onDragEnd(velocityComponent);
+    }
+
+    /** Listener to receive updates on the swipe. */
+    public interface Listener {
+        void onDragStart(boolean start);
+
+        // TODO remove
+        boolean onDrag(float displacement);
+
+        default boolean onDrag(float displacement, MotionEvent event) {
+            return onDrag(displacement);
+        }
+
+        void onDragEnd(float velocity);
+    }
+
+    public abstract static class Direction {
+
+        abstract boolean isPositive(float displacement);
+
+        abstract boolean isNegative(float displacement);
+
+        /** Returns the part of the given {@link PointF} that is relevant to this direction. */
+        abstract float extractDirection(PointF point);
+
+        /** Reject cases where the angle or slop condition is not met. */
+        abstract boolean canScrollStart(PointF displacement, float touchSlop);
+
+    }
+}
diff --git a/src/com/android/launcher3/touch/SwipeDetector.java b/src/com/android/launcher3/touch/SwipeDetector.java
deleted file mode 100644
index c38ca24..0000000
--- a/src/com/android/launcher3/touch/SwipeDetector.java
+++ /dev/null
@@ -1,391 +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.touch;
-
-import static android.view.MotionEvent.INVALID_POINTER_ID;
-
-import android.content.Context;
-import android.graphics.PointF;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.ViewConfiguration;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.launcher3.Utilities;
-
-/**
- * One dimensional scroll/drag/swipe gesture detector.
- *
- * Definition of swipe is different from android system in that this detector handles
- * 'swipe to dismiss', 'swiping up/down a container' but also keeps scrolling state before
- * swipe action happens
- */
-public class SwipeDetector {
-
-    private static final boolean DBG = false;
-    private static final String TAG = "SwipeDetector";
-    private static final float ANIMATION_DURATION = 1200;
-    /** The minimum release velocity in pixels per millisecond that triggers fling.*/
-    private static final float RELEASE_VELOCITY_PX_MS = 1.0f;
-
-    public static final int DIRECTION_POSITIVE = 1 << 0;
-    public static final int DIRECTION_NEGATIVE = 1 << 1;
-    public static final int DIRECTION_BOTH = DIRECTION_NEGATIVE | DIRECTION_POSITIVE;
-
-    public static final Direction VERTICAL = new Direction() {
-
-        @Override
-        float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint, boolean isRtl) {
-            return ev.getY(pointerIndex) - refPoint.y;
-        }
-
-        @Override
-        float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
-            return Math.abs(ev.getX(pointerIndex) - downPos.x);
-        }
-
-        @Override
-        float getVelocity(VelocityTracker tracker, boolean isRtl) {
-            return tracker.getYVelocity();
-        }
-
-        @Override
-        boolean isPositive(float displacement) {
-            // Up
-            return displacement < 0;
-        }
-
-        @Override
-        boolean isNegative(float displacement) {
-            // Down
-            return displacement > 0;
-        }
-    };
-
-    public static final Direction HORIZONTAL = new Direction() {
-
-        @Override
-        float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint, boolean isRtl) {
-            float displacement = ev.getX(pointerIndex) - refPoint.x;
-            if (isRtl) {
-                displacement = -displacement;
-            }
-            return displacement;
-        }
-
-        @Override
-        float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
-            return Math.abs(ev.getY(pointerIndex) - downPos.y);
-        }
-
-        @Override
-        float getVelocity(VelocityTracker tracker, boolean isRtl) {
-            float velocity = tracker.getXVelocity();
-            if (isRtl) {
-                velocity = -velocity;
-            }
-            return velocity;
-        }
-
-        @Override
-        boolean isPositive(float displacement) {
-            // Right
-            return displacement > 0;
-        }
-
-        @Override
-        boolean isNegative(float displacement) {
-            // Left
-            return displacement < 0;
-        }
-    };
-
-    private final PointF mDownPos = new PointF();
-    private final PointF mLastPos = new PointF();
-    private final Direction mDir;
-    private final boolean mIsRtl;
-    private final float mTouchSlop;
-    private final float mMaxVelocity;
-    /* Client of this gesture detector can register a callback. */
-    private final Listener mListener;
-
-    private int mActivePointerId = INVALID_POINTER_ID;
-    private VelocityTracker mVelocityTracker;
-    private float mLastDisplacement;
-    private float mDisplacement;
-    private float mSubtractDisplacement;
-    private boolean mIgnoreSlopWhenSettling;
-    private int mScrollDirections;
-    private ScrollState mState = ScrollState.IDLE;
-
-    private enum ScrollState {
-        IDLE,
-        DRAGGING,      // onDragStart, onDrag
-        SETTLING       // onDragEnd
-    }
-
-    public SwipeDetector(@NonNull Context context, @NonNull Listener l, @NonNull Direction dir) {
-        this(ViewConfiguration.get(context), l, dir, Utilities.isRtl(context.getResources()));
-    }
-
-    @VisibleForTesting
-    protected SwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
-            @NonNull Direction dir, boolean isRtl) {
-        mListener = l;
-        mDir = dir;
-        mIsRtl = isRtl;
-        mTouchSlop = config.getScaledTouchSlop();
-        mMaxVelocity = config.getScaledMaximumFlingVelocity();
-    }
-
-    public static long calculateDuration(float velocity, float progressNeeded) {
-        // TODO: make these values constants after tuning.
-        float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity));
-        float travelDistance = Math.max(0.2f, progressNeeded);
-        long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
-        if (DBG) {
-            Log.d(TAG, String.format(
-                    "calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded));
-        }
-        return duration;
-    }
-
-    public int getDownX() {
-        return (int) mDownPos.x;
-    }
-
-    public int getDownY() {
-        return (int) mDownPos.y;
-    }
-    /**
-     * There's no touch and there's no animation.
-     */
-    public boolean isIdleState() {
-        return mState == ScrollState.IDLE;
-    }
-
-    public boolean isSettlingState() {
-        return mState == ScrollState.SETTLING;
-    }
-
-    public boolean isDraggingState() {
-        return mState == ScrollState.DRAGGING;
-    }
-
-    public boolean isDraggingOrSettling() {
-        return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
-    }
-
-    public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
-        mScrollDirections = scrollDirectionFlags;
-        mIgnoreSlopWhenSettling = ignoreSlop;
-    }
-
-    public int getScrollDirections() {
-        return mScrollDirections;
-    }
-
-    public void finishedScrolling() {
-        setState(ScrollState.IDLE);
-    }
-
-    /**
-     * Returns if the start drag was towards the positive direction or negative.
-     *
-     * @see #setDetectableScrollConditions(int, boolean)
-     * @see #DIRECTION_BOTH
-     */
-    public boolean wasInitialTouchPositive() {
-        return mDir.isPositive(mSubtractDisplacement);
-    }
-
-    public boolean onTouchEvent(MotionEvent ev) {
-        int actionMasked = ev.getActionMasked();
-        if (actionMasked == MotionEvent.ACTION_DOWN && mVelocityTracker != null) {
-            mVelocityTracker.clear();
-        }
-        if (mVelocityTracker == null) {
-            mVelocityTracker = VelocityTracker.obtain();
-        }
-        mVelocityTracker.addMovement(ev);
-
-        switch (actionMasked) {
-            case MotionEvent.ACTION_DOWN:
-                mActivePointerId = ev.getPointerId(0);
-                mDownPos.set(ev.getX(), ev.getY());
-                mLastPos.set(mDownPos);
-                mLastDisplacement = 0;
-                mDisplacement = 0;
-
-                if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
-                    setState(ScrollState.DRAGGING);
-                }
-                break;
-            //case MotionEvent.ACTION_POINTER_DOWN:
-            case MotionEvent.ACTION_POINTER_UP:
-                int ptrIdx = ev.getActionIndex();
-                int ptrId = ev.getPointerId(ptrIdx);
-                if (ptrId == mActivePointerId) {
-                    final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
-                    mDownPos.set(
-                            ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
-                            ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
-                    mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
-                    mActivePointerId = ev.getPointerId(newPointerIdx);
-                }
-                break;
-            case MotionEvent.ACTION_MOVE:
-                int pointerIndex = ev.findPointerIndex(mActivePointerId);
-                if (pointerIndex == INVALID_POINTER_ID) {
-                    break;
-                }
-                mDisplacement = mDir.getDisplacement(ev, pointerIndex, mDownPos, mIsRtl);
-
-                // handle state and listener calls.
-                if (mState != ScrollState.DRAGGING && shouldScrollStart(ev, pointerIndex)) {
-                    setState(ScrollState.DRAGGING);
-                }
-                if (mState == ScrollState.DRAGGING) {
-                    reportDragging(ev);
-                }
-                mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
-                break;
-            case MotionEvent.ACTION_CANCEL:
-            case MotionEvent.ACTION_UP:
-                // These are synthetic events and there is no need to update internal values.
-                if (mState == ScrollState.DRAGGING) {
-                    setState(ScrollState.SETTLING);
-                }
-                mVelocityTracker.recycle();
-                mVelocityTracker = null;
-                break;
-            default:
-                break;
-        }
-        return true;
-    }
-
-    //------------------- ScrollState transition diagram -----------------------------------
-    //
-    // IDLE -> (mDisplacement > mTouchSlop) -> DRAGGING
-    // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING
-    // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING
-    // SETTLING -> (View settled) -> IDLE
-
-    private void setState(ScrollState newState) {
-        if (DBG) {
-            Log.d(TAG, "setState:" + mState + "->" + newState);
-        }
-        // onDragStart and onDragEnd is reported ONLY on state transition
-        if (newState == ScrollState.DRAGGING) {
-            initializeDragging();
-            if (mState == ScrollState.IDLE) {
-                reportDragStart(false /* recatch */);
-            } else if (mState == ScrollState.SETTLING) {
-                reportDragStart(true /* recatch */);
-            }
-        }
-        if (newState == ScrollState.SETTLING) {
-            reportDragEnd();
-        }
-
-        mState = newState;
-    }
-
-    private boolean shouldScrollStart(MotionEvent ev, int pointerIndex) {
-        // reject cases where the angle or slop condition is not met.
-        if (Math.max(mDir.getActiveTouchSlop(ev, pointerIndex, mDownPos), mTouchSlop)
-                > Math.abs(mDisplacement)) {
-            return false;
-        }
-
-        // Check if the client is interested in scroll in current direction.
-        return ((mScrollDirections & DIRECTION_NEGATIVE) > 0 && mDir.isNegative(mDisplacement))
-                || ((mScrollDirections & DIRECTION_POSITIVE) > 0 && mDir.isPositive(mDisplacement));
-    }
-
-    private void reportDragStart(boolean recatch) {
-        mListener.onDragStart(!recatch);
-        if (DBG) {
-            Log.d(TAG, "onDragStart recatch:" + recatch);
-        }
-    }
-
-    private void initializeDragging() {
-        if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
-            mSubtractDisplacement = 0;
-        } else if (mDisplacement > 0) {
-            mSubtractDisplacement = mTouchSlop;
-        } else {
-            mSubtractDisplacement = -mTouchSlop;
-        }
-    }
-
-    private void reportDragging(MotionEvent event) {
-        if (mDisplacement != mLastDisplacement) {
-            if (DBG) {
-                Log.d(TAG, String.format("onDrag disp=%.1f", mDisplacement));
-            }
-
-            mLastDisplacement = mDisplacement;
-            mListener.onDrag(mDisplacement - mSubtractDisplacement, event);
-        }
-    }
-
-    private void reportDragEnd() {
-        mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
-        float velocity = mDir.getVelocity(mVelocityTracker, mIsRtl) / 1000;
-        if (DBG) {
-            Log.d(TAG, String.format("onScrollEnd disp=%.1f, velocity=%.1f",
-                    mDisplacement, velocity));
-        }
-
-        mListener.onDragEnd(velocity, Math.abs(velocity) > RELEASE_VELOCITY_PX_MS);
-    }
-
-    /** Listener to receive updates on the swipe. */
-    public interface Listener {
-        void onDragStart(boolean start);
-
-        boolean onDrag(float displacement);
-
-        default boolean onDrag(float displacement, MotionEvent event) {
-            return onDrag(displacement);
-        }
-
-        void onDragEnd(float velocity, boolean fling);
-    }
-
-    public abstract static class Direction {
-
-        abstract float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint,
-                boolean isRtl);
-
-        /**
-         * Distance in pixels a touch can wander before we think the user is scrolling.
-         */
-        abstract float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos);
-
-        abstract float getVelocity(VelocityTracker tracker, boolean isRtl);
-
-        abstract boolean isPositive(float displacement);
-
-        abstract boolean isNegative(float displacement);
-    }
-}
diff --git a/src/com/android/launcher3/util/ActivityTracker.java b/src/com/android/launcher3/util/ActivityTracker.java
index b4f361f..4b931f4 100644
--- a/src/com/android/launcher3/util/ActivityTracker.java
+++ b/src/com/android/launcher3/util/ActivityTracker.java
@@ -45,13 +45,7 @@
     }
 
     public void onActivityDestroyed(T activity) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE, "onActivityDestroyed");
-        }
         if (mCurrentActivity.get() == activity) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE, "onActivityDestroyed: clear");
-            }
             mCurrentActivity.clear();
         }
     }
@@ -97,10 +91,6 @@
     }
 
     public boolean handleCreate(T activity) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE,
-                    "ActivityTracker.handleCreate " + mCurrentActivity.get() + " => " + activity);
-        }
         mCurrentActivity = new WeakReference<>(activity);
         return handleIntent(activity, activity.getIntent(), false, false);
     }
diff --git a/src/com/android/launcher3/util/LooperIdleLock.java b/src/com/android/launcher3/util/LooperIdleLock.java
index 2896535..f4ccf42 100644
--- a/src/com/android/launcher3/util/LooperIdleLock.java
+++ b/src/com/android/launcher3/util/LooperIdleLock.java
@@ -22,29 +22,30 @@
 /**
  * Utility class to block execution until the UI looper is idle.
  */
-public class LooperIdleLock implements MessageQueue.IdleHandler, Runnable {
+public class LooperIdleLock implements MessageQueue.IdleHandler {
 
     private final Object mLock;
 
     private boolean mIsLocked;
+    private Looper mLooper;
 
     public LooperIdleLock(Object lock, Looper looper) {
         mLock = lock;
+        mLooper = looper;
         mIsLocked = true;
         looper.getQueue().addIdleHandler(this);
     }
 
     @Override
-    public void run() {
-        Looper.myQueue().addIdleHandler(this);
-    }
-
-    @Override
     public boolean queueIdle() {
         synchronized (mLock) {
             mIsLocked = false;
             mLock.notify();
         }
+        // Manually remove from the list in case we're calling this outside of the idle callbacks
+        // (this is Ok in the normal flow as well because MessageQueue makes a copy of all handlers
+        // before calling back)
+        mLooper.getQueue().removeIdleHandler(this);
         return false;
     }
 
diff --git a/src/com/android/launcher3/util/UiThreadHelper.java b/src/com/android/launcher3/util/UiThreadHelper.java
index a133f01..ec87e79 100644
--- a/src/com/android/launcher3/util/UiThreadHelper.java
+++ b/src/com/android/launcher3/util/UiThreadHelper.java
@@ -23,7 +23,6 @@
 import android.os.IBinder;
 import android.os.Message;
 import android.view.inputmethod.InputMethodManager;
-import com.android.launcher3.uioverrides.UiFactory;
 
 /**
  * Utility class for offloading some class from UI thread
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
new file mode 100644
index 0000000..04741a1
--- /dev/null
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -0,0 +1,84 @@
+/*
+ * 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.util;
+
+import static android.os.VibrationEffect.createPredefined;
+import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Build;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.provider.Settings;
+
+/**
+ * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary.
+ */
+@TargetApi(Build.VERSION_CODES.Q)
+public class VibratorWrapper {
+
+    public static final MainThreadInitializedObject<VibratorWrapper> INSTANCE =
+            new MainThreadInitializedObject<>(VibratorWrapper::new);
+
+    private static final VibrationEffect EFFECT_CLICK =
+            createPredefined(VibrationEffect.EFFECT_CLICK);
+
+    /**
+     * Haptic when entering overview.
+     */
+    public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK;
+
+    private final Vibrator mVibrator;
+    private final boolean mHasVibrator;
+
+    private boolean mIsHapticFeedbackEnabled;
+
+    public VibratorWrapper(Context context) {
+        mVibrator = context.getSystemService(Vibrator.class);
+        mHasVibrator = mVibrator.hasVibrator();
+        if (mHasVibrator) {
+            final ContentResolver resolver = context.getContentResolver();
+            mIsHapticFeedbackEnabled = isHapticFeedbackEnabled(resolver);
+            final ContentObserver observer = new ContentObserver(MAIN_EXECUTOR.getHandler()) {
+                @Override
+                public void onChange(boolean selfChange) {
+                    mIsHapticFeedbackEnabled = isHapticFeedbackEnabled(resolver);
+                }
+            };
+            resolver.registerContentObserver(Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED),
+                    false /* notifyForDescendents */, observer);
+        } else {
+            mIsHapticFeedbackEnabled = false;
+        }
+    }
+
+    private boolean isHapticFeedbackEnabled(ContentResolver resolver) {
+        return Settings.System.getInt(resolver, HAPTIC_FEEDBACK_ENABLED, 0) == 1;
+    }
+
+    /** Vibrates with the given effect if haptic feedback is available and enabled. */
+    public void vibrate(VibrationEffect vibrationEffect) {
+        if (mHasVibrator && mIsHapticFeedbackEnabled) {
+            UI_HELPER_EXECUTOR.execute(() -> mVibrator.vibrate(vibrationEffect));
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/ViewOnDrawExecutor.java b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
index 61ba4e5..5a131c8 100644
--- a/src/com/android/launcher3/util/ViewOnDrawExecutor.java
+++ b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
@@ -55,7 +55,9 @@
             mLoadAnimationCompleted = true;
         }
 
-        attachObserver();
+        if (mAttachedView.isAttachedToWindow()) {
+            attachObserver();
+        }
     }
 
     private void attachObserver() {
diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java
index a4518ba..195a77a 100644
--- a/src/com/android/launcher3/views/AbstractSlideInView.java
+++ b/src/com/android/launcher3/views/AbstractSlideInView.java
@@ -32,13 +32,14 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.BaseSwipeDetector;
+import com.android.launcher3.touch.SingleAxisSwipeDetector;
 
 /**
  * Extension of AbstractFloatingView with common methods for sliding in from bottom
  */
 public abstract class AbstractSlideInView extends AbstractFloatingView
-        implements SwipeDetector.Listener {
+        implements SingleAxisSwipeDetector.Listener {
 
     protected static Property<AbstractSlideInView, Float> TRANSLATION_SHIFT =
             new Property<AbstractSlideInView, Float>(Float.class, "translationShift") {
@@ -57,7 +58,7 @@
     protected static final float TRANSLATION_SHIFT_OPENED = 0f;
 
     protected final Launcher mLauncher;
-    protected final SwipeDetector mSwipeDetector;
+    protected final SingleAxisSwipeDetector mSwipeDetector;
     protected final ObjectAnimator mOpenCloseAnimator;
 
     protected View mContent;
@@ -73,7 +74,8 @@
         mLauncher = Launcher.getLauncher(context);
 
         mScrollInterpolator = Interpolators.SCROLL_CUBIC;
-        mSwipeDetector = new SwipeDetector(context, this, SwipeDetector.VERTICAL);
+        mSwipeDetector = new SingleAxisSwipeDetector(context, this,
+                SingleAxisSwipeDetector.VERTICAL);
 
         mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this);
         mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
@@ -97,7 +99,7 @@
         }
 
         int directionsToDetectScroll = mSwipeDetector.isIdleState() ?
-                SwipeDetector.DIRECTION_NEGATIVE : 0;
+                SingleAxisSwipeDetector.DIRECTION_NEGATIVE : 0;
         mSwipeDetector.setDetectableScrollConditions(
                 directionsToDetectScroll, false);
         mSwipeDetector.onTouchEvent(ev);
@@ -122,7 +124,7 @@
         return mIsOpen && mOpenCloseAnimator.isRunning();
     }
 
-    /* SwipeDetector.Listener */
+    /* SingleAxisSwipeDetector.Listener */
 
     @Override
     public void onDragStart(boolean start) { }
@@ -136,17 +138,17 @@
     }
 
     @Override
-    public void onDragEnd(float velocity, boolean fling) {
-        if ((fling && velocity > 0) || mTranslationShift > 0.5f) {
+    public void onDragEnd(float velocity) {
+        if ((mSwipeDetector.isFling(velocity) && velocity > 0) || mTranslationShift > 0.5f) {
             mScrollInterpolator = scrollInterpolatorForVelocity(velocity);
-            mOpenCloseAnimator.setDuration(SwipeDetector.calculateDuration(
+            mOpenCloseAnimator.setDuration(BaseSwipeDetector.calculateDuration(
                     velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift));
             close(true);
         } else {
             mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat(
                     TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
             mOpenCloseAnimator.setDuration(
-                    SwipeDetector.calculateDuration(velocity, mTranslationShift))
+                    BaseSwipeDetector.calculateDuration(velocity, mTranslationShift))
                     .setInterpolator(Interpolators.DEACCEL);
             mOpenCloseAnimator.start();
         }
diff --git a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
new file mode 100644
index 0000000..60eb304
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
@@ -0,0 +1,44 @@
+/*
+ * 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.systemui.plugins;
+
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
+/**
+ * Implement this interface to receive a callback when the user swipes right
+ * to left on the gesture area. It won't fire if the user has quick switched to a previous app
+ * (swiped right) and the current app isn't yet the active one (i.e., if swiping left would take
+ * the user to a more recent app).
+ */
+@ProvidesInterface(action = com.android.systemui.plugins.OverscrollPlugin.ACTION,
+        version = com.android.systemui.plugins.OverlayPlugin.VERSION)
+public interface OverscrollPlugin extends Plugin {
+
+    String ACTION = "com.android.systemui.action.PLUGIN_LAUNCHER_OVERSCROLL";
+    int VERSION = 1;
+
+    String DEVICE_STATE_LOCKED = "Locked";
+    String DEVICE_STATE_LAUNCHER = "Launcher";
+    String DEVICE_STATE_APP = "App";
+    String DEVICE_STATE_UNKNOWN = "Unknown";
+
+    /**
+     * Called when the user completed a right to left swipe in the gesture area.
+     *
+     * @param deviceState One of the DEVICE_STATE_* constants.
+     */
+    void onOverscroll(String deviceState);
+}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
new file mode 100644
index 0000000..5407ea3
--- /dev/null
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -0,0 +1,36 @@
+/*
+ * 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.app.Activity;
+import android.app.Person;
+import android.content.pm.ShortcutInfo;
+
+import com.android.launcher3.Utilities;
+
+import java.io.PrintWriter;
+
+public class ApiWrapper {
+
+    public static boolean dumpActivity(Activity activity, PrintWriter writer) {
+        return false;
+    }
+
+    public static Person[] getPersons(ShortcutInfo si) {
+        return Utilities.EMPTY_PERSON_ARRAY;
+    }
+}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java b/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
deleted file mode 100644
index 606c990..0000000
--- a/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
+++ /dev/null
@@ -1,102 +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.app.Activity;
-import android.app.Person;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentSender;
-import android.content.pm.ShortcutInfo;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
-import com.android.launcher3.LauncherStateManager.StateHandler;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.graphics.RotationMode;
-import com.android.launcher3.util.TouchController;
-
-import java.io.PrintWriter;
-
-public class UiFactory {
-
-    public static TouchController[] createTouchControllers(Launcher launcher) {
-        return new TouchController[] {
-                launcher.getDragController(), new AllAppsSwipeController(launcher)};
-    }
-
-    public static Runnable enableLiveUIChanges(Launcher l) {
-        return null;
-    }
-
-    public static StateHandler[] getStateHandler(Launcher launcher) {
-        return new StateHandler[] {
-                launcher.getAllAppsController(), launcher.getWorkspace() };
-    }
-
-    public static void resetOverview(Launcher launcher) { }
-
-    public static void onLauncherStateOrFocusChanged(Launcher launcher) { }
-
-    public static void onCreate(Launcher launcher) { }
-
-    public static void onStart(Launcher launcher) { }
-
-    public static void onEnterAnimationComplete(Context context) {}
-
-    public static void onLauncherStateOrResumeChanged(Launcher launcher) { }
-
-    public static void onTrimMemory(Launcher launcher, int level) { }
-
-    public static void useFadeOutAnimationForLauncherStart(Launcher launcher,
-            CancellationSignal cancellationSignal) { }
-
-    public static boolean dumpActivity(Activity activity, PrintWriter writer) {
-        return false;
-    }
-
-    public static void setBackButtonAlpha(Launcher launcher, float alpha, boolean animate) { }
-
-
-    public static ScaleAndTranslation getOverviewScaleAndTranslationForNormalState(Launcher l) {
-        return new ScaleAndTranslation(1.1f, 0f, 0f);
-    }
-
-    public static RotationMode getRotationMode(DeviceProfile dp) {
-        return RotationMode.NORMAL;
-    }
-
-    public static boolean startIntentSenderForResult(Activity activity, IntentSender intent,
-            int requestCode, Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags,
-            Bundle options) {
-        return false;
-    }
-
-    public static boolean startActivityForResult(Activity activity, Intent intent, int requestCode,
-            Bundle options) {
-        return false;
-    }
-
-    public static void resetPendingActivityResults(Launcher launcher, int requestCode) { }
-
-    public static Person[] getPersons(ShortcutInfo si) {
-        return Utilities.EMPTY_PERSON_ARRAY;
-    }
-}
diff --git a/tests/src/com/android/launcher3/touch/SwipeDetectorTest.java b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
similarity index 72%
rename from tests/src/com/android/launcher3/touch/SwipeDetectorTest.java
rename to tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
index f209fae..5174e4d 100644
--- a/tests/src/com/android/launcher3/touch/SwipeDetectorTest.java
+++ b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
@@ -15,6 +15,12 @@
  */
 package com.android.launcher3.touch;
 
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
+import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
+
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyFloat;
 import static org.mockito.Matchers.anyObject;
@@ -39,19 +45,19 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class SwipeDetectorTest {
+public class SingleAxisSwipeDetectorTest {
 
-    private static final String TAG = SwipeDetectorTest.class.getSimpleName();
+    private static final String TAG = SingleAxisSwipeDetectorTest.class.getSimpleName();
     public static void L(String s, Object... parts) {
         Log.d(TAG, (parts.length == 0) ? s : String.format(s, parts));
     }
 
     private TouchEventGenerator mGenerator;
-    private SwipeDetector mDetector;
+    private SingleAxisSwipeDetector mDetector;
     private int mTouchSlop;
 
     @Mock
-    private SwipeDetector.Listener mMockListener;
+    private SingleAxisSwipeDetector.Listener mMockListener;
 
     @Mock
     private ViewConfiguration mMockConfig;
@@ -65,8 +71,8 @@
         doReturn(orgConfig.getScaledMaximumFlingVelocity()).when(mMockConfig)
                 .getScaledMaximumFlingVelocity();
 
-        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.VERTICAL, false);
-        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, false);
+        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false);
+        mDetector.setDetectableScrollConditions(DIRECTION_BOTH, false);
         mTouchSlop = orgConfig.getScaledTouchSlop();
         doReturn(mTouchSlop).when(mMockConfig).getScaledTouchSlop();
 
@@ -75,8 +81,8 @@
 
     @Test
     public void testDragStart_verticalPositive() {
-        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.VERTICAL, false);
-        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
+        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false);
+        mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100, 100 - mTouchSlop);
         // TODO: actually calculate the following parameters and do exact value checks.
@@ -85,8 +91,8 @@
 
     @Test
     public void testDragStart_verticalNegative() {
-        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.VERTICAL, false);
-        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_NEGATIVE, false);
+        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false);
+        mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100, 100 + mTouchSlop);
         // TODO: actually calculate the following parameters and do exact value checks.
@@ -103,8 +109,8 @@
 
     @Test
     public void testDragStart_horizontalPositive() {
-        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, false);
-        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
+        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, false);
+        mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
 
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 + mTouchSlop, 100);
@@ -114,8 +120,8 @@
 
     @Test
     public void testDragStart_horizontalNegative() {
-        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, false);
-        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_NEGATIVE, false);
+        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, false);
+        mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
 
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 - mTouchSlop, 100);
@@ -125,8 +131,8 @@
 
     @Test
     public void testDragStart_horizontalRtlPositive() {
-        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, true);
-        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
+        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, true);
+        mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
 
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 - mTouchSlop, 100);
@@ -136,8 +142,8 @@
 
     @Test
     public void testDragStart_horizontalRtlNegative() {
-        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, true);
-        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_NEGATIVE, false);
+        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, true);
+        mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
 
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 + mTouchSlop, 100);
@@ -160,6 +166,6 @@
         mGenerator.move(0, 100, 100 + mTouchSlop * 2);
         mGenerator.lift(0);
         // TODO: actually calculate the following parameters and do exact value checks.
-        verify(mMockListener).onDragEnd(anyFloat(), anyBoolean());
+        verify(mMockListener).onDragEnd(anyFloat());
     }
 }
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 62989a3..76b5d28 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -26,6 +26,8 @@
 
 import static java.lang.System.exit;
 
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -373,7 +375,8 @@
         startIntent(
                 getInstrumentation().getContext().getPackageManager().getLaunchIntentForPackage(
                         packageName),
-                By.pkg(packageName).depth(0));
+                By.pkg(packageName).depth(0),
+                true /* newTask */);
     }
 
     public static void startTestActivity(int activityNumber) {
@@ -382,12 +385,17 @@
                 getLaunchIntentForPackage(packageName);
         intent.setComponent(new ComponentName(packageName,
                 "com.android.launcher3.tests.Activity" + activityNumber));
-        startIntent(intent, By.pkg(packageName).text("TestActivity" + activityNumber));
+        startIntent(intent, By.pkg(packageName).text("TestActivity" + activityNumber),
+                false /* newTask */);
     }
 
-    private static void startIntent(Intent intent, BySelector selector) {
+    private static void startIntent(Intent intent, BySelector selector, boolean newTask) {
         intent.addCategory(Intent.CATEGORY_LAUNCHER);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        if (newTask) {
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        } else {
+            intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+        }
         getInstrumentation().getTargetContext().startActivity(intent);
         assertTrue("App didn't start: " + selector,
                 UiDevice.getInstance(getInstrumentation())
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 465cee2..29da0fa 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -18,6 +18,10 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_PRESUBMIT;
+import static com.android.launcher3.util.rule.TestStabilityRule.RUN_FLAFOR;
+import static com.android.launcher3.util.rule.TestStabilityRule.UNBUNDLED_PRESUBMIT;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -174,7 +178,8 @@
     @Test
     public void testWorkspace() throws Exception {
         // b/142828227
-        if (android.os.Build.MODEL.contains("Cuttlefish") && TestHelpers.isInLauncherProcess()) {
+        if (android.os.Build.MODEL.contains("Cuttlefish") && TestHelpers.isInLauncherProcess() &&
+                (RUN_FLAFOR & (PLATFORM_PRESUBMIT | UNBUNDLED_PRESUBMIT)) != 0) {
             return;
         }
         final Workspace workspace = mLauncher.getWorkspace();
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index 1c17e34..259f9ed 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -48,7 +48,6 @@
 
     @Test
     @PortraitLandscape
-    @Stability(flavors = UNBUNDLED_POSTSUBMIT) // b/142514365
     public void testDragIcon() throws Throwable {
         clearHomescreen();
         mDevice.pressHome();
diff --git a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
index 69bf01d..08fa098 100644
--- a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
@@ -57,7 +57,7 @@
     public static final int PLATFORM_PRESUBMIT = 0x8;
     public static final int PLATFORM_POSTSUBMIT = 0x10;
 
-    private static final int RUN_FLAFOR = getRunFlavor();
+    public static final int RUN_FLAFOR = getRunFlavor();
 
     @Retention(RetentionPolicy.RUNTIME)
     @Target(ElementType.METHOD)
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index e1e9b8d..cc92327 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -163,7 +163,7 @@
                         "Exceeded max scroll attempts: " + MAX_SCROLL_ATTEMPTS,
                         ++attempts <= MAX_SCROLL_ATTEMPTS);
 
-                mLauncher.scroll(allAppsContainer, Direction.UP, margins, 50);
+                mLauncher.scroll(allAppsContainer, Direction.UP, margins, 12);
             }
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("scrolled up")) {
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 41a4bdb..3713c14 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -424,11 +424,7 @@
         // b/136278866
         for (int i = 0; i != 100; ++i) {
             if (getNavigationModeMismatchError() == null) break;
-            try {
-                Thread.sleep(100);
-            } catch (InterruptedException e) {
-                e.printStackTrace();
-            }
+            sleep(100);
         }
 
         final String error = getNavigationModeMismatchError();
@@ -497,9 +493,7 @@
     }
 
     public void waitForLauncherInitialized() {
-        // b/136278866
-        final int attempts = android.os.Build.MODEL.contains("Cuttlefish") ? 600 : 100;
-        for (int i = 0; i < attempts; ++i) {
+        for (int i = 0; i < 100; ++i) {
             if (getTestInfo(
                     TestProtocol.REQUEST_IS_LAUNCHER_INITIALIZED).
                     getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD)) {
@@ -797,6 +791,8 @@
         final int distance = gestureStart - container.getVisibleBounds().top - topPadding;
         final int bottomMargin = container.getVisibleBounds().height() - distance;
 
+        // TODO: Make the gesture steps dependent on the distance so that it can run for various
+        //       screen sizes
         scroll(
                 container,
                 Direction.DOWN,
@@ -805,7 +801,7 @@
                         0,
                         0,
                         Math.max(bottomMargin, getBottomGestureMargin(container))),
-                150);
+                80);
     }
 
     void scroll(UiObject2 container, Direction direction, Rect margins, int steps) {
diff --git a/tests/tapl/com/android/launcher3/tapl/Overview.java b/tests/tapl/com/android/launcher3/tapl/Overview.java
index 4f8aeb1..16a64a7 100644
--- a/tests/tapl/com/android/launcher3/tapl/Overview.java
+++ b/tests/tapl/com/android/launcher3/tapl/Overview.java
@@ -58,7 +58,7 @@
                             getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD),
                     mLauncher.getDevice().getDisplayWidth() / 2,
                     0,
-                    50,
+                    12,
                     ALL_APPS_STATE_ORDINAL);
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index d1261e0..db3314e 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -38,7 +38,7 @@
  * Operations on the workspace screen.
  */
 public final class Workspace extends Home {
-    private static final int DRAG_DURACTION = 2000;
+    private static final int DRAG_DURATION = 500;
     private static final int FLING_STEPS = 10;
     private final UiObject2 mHotseat;
 
@@ -72,7 +72,7 @@
                     start.y,
                     start.x,
                     start.y - swipeHeight - mLauncher.getTouchSlop(),
-                    60,
+                    12,
                     ALL_APPS_STATE_ORDINAL);
 
             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
@@ -166,7 +166,7 @@
         launcher.waitForLauncherObject(longPressIndicator);
         LauncherInstrumentation.log("dragIconToWorkspace: indicator");
         launcher.movePointer(
-                downTime, SystemClock.uptimeMillis(), DRAG_DURACTION, launchableCenter, dest);
+                downTime, SystemClock.uptimeMillis(), DRAG_DURATION, launchableCenter, dest);
         LauncherInstrumentation.log("dragIconToWorkspace: moved pointer");
         launcher.sendPointer(
                 downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest);