Merge "Fix testQuickSwitchFromApp" into ub-launcher3-master
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..2755492 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -96,6 +96,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;
@@ -106,7 +107,6 @@
     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
@@ -135,10 +135,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,7 +148,6 @@
                 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);
@@ -348,7 +348,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/TaskOverlayFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
index 17457aa..298b0ff 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,25 +40,24 @@
 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
     };
 
     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 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;
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..a3a1d6d
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import static 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.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;
+
+}
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 8d7a534..b4df81a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -63,8 +63,6 @@
 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;
@@ -114,8 +112,7 @@
  * Service connected by system-UI for handling touch interaction.
  */
 @TargetApi(Build.VERSION_CODES.Q)
-public class TouchInteractionService extends Service implements
-        NavigationModeChangeListener, PluginListener<OverscrollPlugin> {
+public class TouchInteractionService extends Service implements PluginListener<OverscrollPlugin> {
 
     private static final String TAG = "TouchInteractionService";
 
@@ -269,20 +266,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;
 
         PluginManagerWrapper.INSTANCE.get(getBaseContext()).addPluginListener(this,
@@ -308,7 +303,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) {
@@ -327,12 +322,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();
     }
@@ -369,7 +362,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;
@@ -417,7 +410,6 @@
         }
         disposeEventHandlers();
         mDeviceState.destroy();
-        SysUINavigationMode.INSTANCE.get(this).removeModeChangeListener(this);
         SystemUiProxy.INSTANCE.get(this).setProxy(null);
 
         sConnected = false;
@@ -453,7 +445,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
@@ -494,7 +487,7 @@
                 || 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);
             }
@@ -584,7 +577,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 {
@@ -601,7 +595,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 {
@@ -641,7 +635,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;
         }
@@ -708,7 +702,6 @@
             // Dump everything
             mDeviceState.dump(pw);
             pw.println("TouchState:");
-            pw.println("  navMode=" + mMode);
             boolean resumed = mOverviewComponentObserver != null
                     && mOverviewComponentObserver.getActivityInterface().isResumed();
             pw.println("  resumed=" + resumed);
@@ -748,9 +741,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() {
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 f1b3598..8e9c898 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -157,9 +157,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 +196,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 +440,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 +542,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 +726,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 +747,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 +789,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 +835,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) {
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/views/TaskMenuView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
index 07d0796..b810c4a 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,16 @@
         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.INSTANCE.get(getContext()).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..3af0f70 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,9 @@
                         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.INSTANCE.get(getContext())
+                .getEnabledShortcuts(this)) {
+            info.addAction(s.createAccessibilityAction(context));
         }
 
         if (mDigitalWellBeingToast.hasLimit()) {
@@ -752,16 +746,10 @@
             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.INSTANCE.get(getContext())
+                .getEnabledShortcuts(this)) {
+            if (s.hasHandlerForAction(action)) {
+                s.onClick(this);
                 return true;
             }
         }
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/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 4833c26..8e5d852 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -382,8 +382,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 +393,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 +406,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 +429,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
index dfcc2f8..8b8a4d0 100644
--- a/src/com/android/launcher3/popup/SystemShortcutFactory.java
+++ b/src/com/android/launcher3/popup/SystemShortcutFactory.java
@@ -34,25 +34,24 @@
             forOverride(SystemShortcutFactory.class, R.string.system_shortcut_factory_class);
 
     /** Note that these are in order of priority. */
-    private final SystemShortcut[] mAllShortcuts;
+    private final SystemShortcut.Factory[] mAllFactories;
 
     @SuppressWarnings("unused")
     public SystemShortcutFactory() {
-        this(new SystemShortcut.AppInfo(),
-                new SystemShortcut.Widgets(),
-                new SystemShortcut.Install(),
-                new SystemShortcut.DismissPrediction());
+        this(SystemShortcut.APP_INFO, SystemShortcut.WIDGETS, SystemShortcut.INSTALL,
+                SystemShortcut.DISMISS_PREDICTION);
     }
 
-    protected SystemShortcutFactory(SystemShortcut... shortcuts) {
-        mAllShortcuts = shortcuts;
+    protected SystemShortcutFactory(SystemShortcut.Factory... factories) {
+        mAllFactories = factories;
     }
 
     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);
+        for (SystemShortcut.Factory factory : mAllFactories) {
+            SystemShortcut shortcut = factory.getShortcut(launcher, info);
+            if (shortcut != null) {
+                systemShortcuts.add(shortcut);
             }
         }
 
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 484cbb6..c90c8f6 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();
@@ -803,7 +799,7 @@
                         0,
                         0,
                         Math.max(bottomMargin, getBottomGestureMargin(container))),
-                150);
+                70);
     }
 
     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);