Snap for 6013487 from c5027ce20d5e86c62cb1d0a5cb9778af108e0334 to rvc-release

Change-Id: Idcfc6074032a1874a876ffec88816a4cf95f4eb0
diff --git a/Android.mk b/Android.mk
index 5def65f..3d1d996 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)/quickstep/AndroidManifest-launcher.xml \
+    $(LOCAL_PATH)/AndroidManifest.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)/quickstep/AndroidManifest-launcher.xml \
+    $(LOCAL_PATH)/AndroidManifest.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)/quickstep/AndroidManifest-launcher.xml \
+    $(LOCAL_PATH)/AndroidManifest.xml \
     $(LOCAL_PATH)/AndroidManifest-common.xml
 
 LOCAL_MANIFEST_FILE := quickstep/AndroidManifest.xml
diff --git a/OWNERS b/OWNERS
index 7340e84..538ca33 100644
--- a/OWNERS
+++ b/OWNERS
@@ -10,7 +10,6 @@
 sunnygoyal@google.com
 twickham@google.com
 winsonc@google.com
-zakcohen@google.com
 
 per-file FeatureFlags.java = sunnygoyal@google.com, adamcohen@google.com
 per-file BaseFlags.java = sunnygoyal@google.com, adamcohen@google.com
diff --git a/go/AndroidManifest.xml b/go/AndroidManifest.xml
index f84a82e..fae1eff 100644
--- a/go/AndroidManifest.xml
+++ b/go/AndroidManifest.xml
@@ -22,7 +22,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     package="com.android.launcher3" >
 
-    <uses-sdk android:targetSdkVersion="29" android:minSdkVersion="25"/>
+    <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="25"/>
 
     <application
         android:backupAgent="com.android.launcher3.LauncherBackupAgent"
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/go/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
deleted file mode 100644
index 0c60468..0000000
--- a/go/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ /dev/null
@@ -1,46 +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.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
new file mode 100644
index 0000000..f2aa842
--- /dev/null
+++ b/go/quickstep/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
@@ -0,0 +1,93 @@
+/*
+ * 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;
+    }
+
+    public static void clearSwipeSharedState(Launcher launcher, boolean finishAnimation) { }
+}
diff --git a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
index 04753d2..6b50088 100644
--- a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -46,13 +46,13 @@
         RemoteAnimationProvider {
     private static final String TAG = "AppToOverviewAnimationProvider";
 
-    private final BaseActivityInterface<T> mActivityInterface;
+    private final BaseActivityInterface<T> mHelper;
     private final int mTargetTaskId;
     private IconRecentsView mRecentsView;
     private AppToOverviewAnimationListener mAnimationReadyListener;
 
-    AppToOverviewAnimationProvider(BaseActivityInterface<T> activityInterface, int targetTaskId) {
-        mActivityInterface = activityInterface;
+    AppToOverviewAnimationProvider(BaseActivityInterface<T> helper, int targetTaskId) {
+        mHelper = helper;
         mTargetTaskId = targetTaskId;
     }
 
@@ -68,15 +68,15 @@
     /**
      * Callback for when the activity is ready/initialized.
      *
+     * @param activity the activity that is ready
      * @param wasVisible true if it was visible before
      */
-    boolean onActivityReady(Boolean wasVisible) {
-        T activity = mActivityInterface.getCreatedActivity();
+    boolean onActivityReady(T activity, Boolean wasVisible) {
         if (mAnimationReadyListener != null) {
             mAnimationReadyListener.onActivityReady(activity);
         }
         BaseActivityInterface.AnimationFactory factory =
-                mActivityInterface.prepareRecentsUI(wasVisible,
+                mHelper.prepareRecentsUI(activity, wasVisible,
                         false /* animate activity */, (controller) -> {
                             controller.dispatchOnStart();
                             ValueAnimator anim = controller.getAnimationPlayer()
diff --git a/go/quickstep/src/com/android/quickstep/FallbackActivityInterface.java b/go/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
index ecb9472..2af8441 100644
--- a/go/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/go/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
@@ -29,8 +29,8 @@
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.views.IconRecentsView;
 
+import java.util.function.BiPredicate;
 import java.util.function.Consumer;
-import java.util.function.Predicate;
 
 /**
  * {@link BaseActivityInterface} for recents when the default launcher is different than the
@@ -43,13 +43,12 @@
     public FallbackActivityInterface() { }
 
     @Override
-    public AnimationFactory prepareRecentsUI(boolean activityVisible,
+    public AnimationFactory prepareRecentsUI(RecentsActivity activity, boolean activityVisible,
             boolean animateActivity, Consumer<AnimatorPlaybackController> callback) {
         if (activityVisible) {
             return (transitionLength) -> { };
         }
 
-        RecentsActivity activity = getCreatedActivity();
         IconRecentsView rv = activity.getOverviewPanel();
         rv.setUsingRemoteAnimation(true);
         rv.setAlpha(0);
@@ -85,9 +84,8 @@
 
     @Override
     public ActivityInitListener createActivityInitListener(
-            Predicate<Boolean> onInitListener) {
-        return new ActivityInitListener<>((activity, alreadyOnHome) ->
-                onInitListener.test(alreadyOnHome), RecentsActivity.ACTIVITY_TRACKER);
+            BiPredicate<RecentsActivity, Boolean> onInitListener) {
+        return new ActivityInitListener(onInitListener, RecentsActivity.ACTIVITY_TRACKER);
     }
 
     @Nullable
@@ -117,5 +115,5 @@
     }
 
     @Override
-    public void onLaunchTaskSuccess() { }
+    public void onLaunchTaskSuccess(RecentsActivity activity) { }
 }
diff --git a/go/quickstep/src/com/android/quickstep/GoActivityInterface.java b/go/quickstep/src/com/android/quickstep/GoActivityInterface.java
index b62d17c..5ce0f4c 100644
--- a/go/quickstep/src/com/android/quickstep/GoActivityInterface.java
+++ b/go/quickstep/src/com/android/quickstep/GoActivityInterface.java
@@ -17,7 +17,7 @@
         BaseActivityInterface<T> {
 
     @Override
-    public void onTransitionCancelled(boolean activityVisible) {
+    public void onTransitionCancelled(T activity, boolean activityVisible) {
         // Go transitions to overview are all atomic.
     }
 
@@ -29,7 +29,7 @@
     }
 
     @Override
-    public void onSwipeUpToRecentsComplete() {
+    public void onSwipeUpToRecentsComplete(T activity) {
         // Go does not support swipe up gesture.
     }
 
@@ -39,7 +39,7 @@
     }
 
     @Override
-    public HomeAnimationFactory prepareHomeUI() {
+    public HomeAnimationFactory prepareHomeUI(T activity) {
         // Go does not support gestures from app to home.
         return null;
     }
@@ -63,7 +63,7 @@
     }
 
     @Override
-    public void onLaunchTaskFailed() {
+    public void onLaunchTaskFailed(T activity) {
         // Go does not support gestures from one task to another.
     }
 }
diff --git a/go/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/go/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 3e93480..5bff8e8 100644
--- a/go/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/go/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -26,8 +26,8 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.views.IconRecentsView;
 
+import java.util.function.BiPredicate;
 import java.util.function.Consumer;
-import java.util.function.Predicate;
 
 /**
  * {@link BaseActivityInterface} for the in-launcher recents.
@@ -36,15 +36,15 @@
 public final class LauncherActivityInterface extends GoActivityInterface<Launcher> {
 
     @Override
-    public AnimationFactory prepareRecentsUI(boolean activityVisible, boolean animateActivity,
+    public AnimationFactory prepareRecentsUI(Launcher activity,
+            boolean activityVisible, boolean animateActivity,
             Consumer<AnimatorPlaybackController> callback) {
-        Launcher launcher = getCreatedActivity();
-        LauncherState fromState = launcher.getStateManager().getState();
-        launcher.<IconRecentsView>getOverviewPanel().setUsingRemoteAnimation(true);
+        LauncherState fromState = activity.getStateManager().getState();
+        activity.<IconRecentsView>getOverviewPanel().setUsingRemoteAnimation(true);
         //TODO: Implement this based off where the recents view needs to be for app => recents anim.
         return new AnimationFactory() {
             public void createActivityInterface(long transitionLength) {
-                callback.accept(launcher.getStateManager().createAnimationToNewWorkspace(
+                callback.accept(activity.getStateManager().createAnimationToNewWorkspace(
                         fromState, OVERVIEW, transitionLength));
             }
 
@@ -54,9 +54,9 @@
     }
 
     @Override
-    public LauncherInitListener createActivityInitListener(Predicate<Boolean> onInitListener) {
-        return new LauncherInitListener((activity, alreadyOnHome) ->
-                onInitListener.test(alreadyOnHome));
+    public LauncherInitListener createActivityInitListener(
+            BiPredicate<Launcher, Boolean> onInitListener) {
+        return new LauncherInitListener(onInitListener);
     }
 
     @Override
@@ -105,8 +105,7 @@
     }
 
     @Override
-    public void onLaunchTaskSuccess() {
-        Launcher launcher = getCreatedActivity();
+    public void onLaunchTaskSuccess(Launcher launcher) {
         launcher.getStateManager().moveToRestState();
     }
 }
diff --git a/go/quickstep/src/com/android/quickstep/util/ShelfPeekAnim.java b/go/quickstep/src/com/android/quickstep/util/ShelfPeekAnim.java
deleted file mode 100644
index e7099ec..0000000
--- a/go/quickstep/src/com/android/quickstep/util/ShelfPeekAnim.java
+++ /dev/null
@@ -1,27 +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.quickstep.util;
-
-import com.android.launcher3.Launcher;
-
-/** Empty class, only exists so that lowRamWithQuickstepIconRecentsDebug compiles. */
-public class ShelfPeekAnim {
-    public ShelfPeekAnim(Launcher launcher) {
-    }
-
-    public enum ShelfAnimState {
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
index e380698..87b4d4e 100644
--- a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
+++ b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
@@ -40,7 +40,6 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
-import android.os.UserHandle;
 import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
@@ -67,7 +66,6 @@
 import com.android.launcher3.util.Themes;
 import com.android.quickstep.ContentFillItemAnimator;
 import com.android.quickstep.RecentsModel;
-import com.android.quickstep.RecentsModel.TaskVisualsChangeListener;
 import com.android.quickstep.RecentsToActivityHelper;
 import com.android.quickstep.TaskActionController;
 import com.android.quickstep.TaskAdapter;
@@ -76,7 +74,6 @@
 import com.android.quickstep.TaskSwipeCallback;
 import com.android.quickstep.util.MultiValueUpdateListener;
 import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
@@ -90,8 +87,7 @@
  * Root view for the icon recents view. Acts as the main interface to the rest of the Launcher code
  * base.
  */
-public final class IconRecentsView extends FrameLayout
-        implements Insettable, TaskVisualsChangeListener {
+public final class IconRecentsView extends FrameLayout implements Insettable {
 
     public static final FloatProperty<IconRecentsView> CONTENT_ALPHA =
             new FloatProperty<IconRecentsView>("contentAlpha") {
@@ -163,24 +159,7 @@
     private AnimatorSet mLayoutAnimation;
     private final ArraySet<View> mLayingOutViews = new ArraySet<>();
     private Rect mInsets;
-
-    public IconRecentsView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        mActivity = BaseActivity.fromContext(context);
-        mContext = context;
-        mStatusBarForegroundScrim  =
-                Themes.getAttrDrawable(mContext, R.attr.workspaceStatusBarScrim);
-        mTaskLoader = new TaskListLoader(mContext);
-        mTaskAdapter = new TaskAdapter(mTaskLoader);
-        mTaskAdapter.setOnClearAllClickListener(view -> animateClearAllTasks());
-        mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter,
-                mActivity.getStatsLogManager());
-        mTaskAdapter.setActionController(mTaskActionController);
-        mTaskLayoutManager = new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */);
-    }
-
-    @Override
-    public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
+    private final RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
         ArrayList<TaskItemView> itemViews = getTaskViews();
         for (int i = 0, size = itemViews.size(); i < size; i++) {
             TaskItemView taskView = itemViews.get(i);
@@ -195,10 +174,23 @@
             }
         }
         return null;
-    }
+    };
 
-    @Override
-    public void onTaskIconChanged(String pkg, UserHandle user) { }
+    public IconRecentsView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mActivity = BaseActivity.fromContext(context);
+        mContext = context;
+        mStatusBarForegroundScrim  =
+                Themes.getAttrDrawable(mContext, R.attr.workspaceStatusBarScrim);
+        mTaskLoader = new TaskListLoader(mContext);
+        mTaskAdapter = new TaskAdapter(mTaskLoader);
+        mTaskAdapter.setOnClearAllClickListener(view -> animateClearAllTasks());
+        mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter,
+                mActivity.getStatsLogManager());
+        mTaskAdapter.setActionController(mTaskActionController);
+        mTaskLayoutManager = new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */);
+        RecentsModel.INSTANCE.get(context).addThumbnailChangeListener(listener);
+    }
 
     @Override
     protected void onFinishInflate() {
@@ -283,18 +275,6 @@
     }
 
     @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
-    }
-
-    @Override
     public void setEnabled(boolean enabled) {
         super.setEnabled(enabled);
         int childCount = mTaskRecyclerView.getChildCount();
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
index a3c7c07..5c4f37c 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
@@ -116,7 +116,7 @@
             icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f);
         }
 
-        return BitmapInfo.of(icon, extractColor(icon));
+        return BitmapInfo.fromBitmap(icon, mDisableColorExtractor ? null : mColorExtractor);
     }
 
     public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
@@ -183,10 +183,7 @@
                 bitmap = createIconBitmap(badged, 1f);
             }
         }
-        int color = extractColor(result);
-        return icon instanceof BitmapInfo.Extender
-                ? ((BitmapInfo.Extender) icon).getExtendedInfo(result, color, this)
-                : BitmapInfo.of(result, color);
+        return BitmapInfo.fromBitmap(bitmap, mDisableColorExtractor ? null : mColorExtractor);
     }
 
     public Bitmap createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons) {
@@ -337,10 +334,6 @@
                 iconDpi);
     }
 
-    private int extractColor(Bitmap bitmap) {
-        return mDisableColorExtractor ? 0 : mColorExtractor.findDominantColorByHue(bitmap);
-    }
-
     /**
      * Returns the correct badge size given an icon size
      */
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
index d33f9b1..245561e 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
@@ -18,55 +18,32 @@
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 
-import androidx.annotation.NonNull;
-
 public class BitmapInfo {
 
     public static final Bitmap LOW_RES_ICON = Bitmap.createBitmap(1, 1, Config.ALPHA_8);
-    public static final BitmapInfo LOW_RES_INFO = fromBitmap(LOW_RES_ICON);
 
-    public final Bitmap icon;
-    public final int color;
+    public Bitmap icon;
+    public int color;
 
-    public BitmapInfo(Bitmap icon, int color) {
-        this.icon = icon;
-        this.color = color;
-    }
-
-    /**
-     * Ideally icon should not be null, except in cases when generating hardware bitmap failed
-     */
-    public final boolean isNullOrLowRes() {
-        return icon == null || icon == LOW_RES_ICON;
+    public void applyTo(BitmapInfo info) {
+        info.icon = icon;
+        info.color = color;
     }
 
     public final boolean isLowRes() {
         return LOW_RES_ICON == icon;
     }
 
-    public static BitmapInfo fromBitmap(@NonNull Bitmap bitmap) {
-        return of(bitmap, 0);
+    public static BitmapInfo fromBitmap(Bitmap bitmap) {
+        return fromBitmap(bitmap, null);
     }
 
-    public static BitmapInfo of(@NonNull Bitmap bitmap, int color) {
-        return new BitmapInfo(bitmap, color);
-    }
-
-    /**
-     * Interface to be implemented by drawables to provide a custom BitmapInfo
-     */
-    public interface Extender {
-
-        /**
-         * Called for creating a custom BitmapInfo
-         */
-        default BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory) {
-            return BitmapInfo.of(bitmap, color);
-        }
-
-        /**
-         * Notifies the drawable that it will be drawn directly in the UI, without any preprocessing
-         */
-        default void prepareToDrawOnUi() { }
+    public static BitmapInfo fromBitmap(Bitmap bitmap, ColorExtractor dominantColorExtractor) {
+        BitmapInfo info = new BitmapInfo();
+        info.icon = bitmap;
+        info.color = dominantColorExtractor != null
+                ? dominantColorExtractor.findDominantColorByHue(bitmap)
+                : 0;
+        return info;
     }
 }
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
index 6f63d88..93f0538 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
@@ -71,10 +71,7 @@
     // Empty class name is used for storing package default entry.
     public static final String EMPTY_CLASS_NAME = ".";
 
-    public static class CacheEntry {
-
-        @NonNull
-        public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO;
+    public static class CacheEntry extends BitmapInfo {
         public CharSequence title = "";
         public CharSequence contentDescription = "";
     }
@@ -262,23 +259,23 @@
         if (!replaceExisting) {
             entry = mCache.get(key);
             // We can't reuse the entry if the high-res icon is not present.
-            if (entry == null || entry.bitmap.isNullOrLowRes()) {
+            if (entry == null || entry.icon == null || entry.isLowRes()) {
                 entry = null;
             }
         }
         if (entry == null) {
             entry = new CacheEntry();
-            entry.bitmap = cachingLogic.loadIcon(mContext, object);
+            cachingLogic.loadIcon(mContext, object, entry);
         }
         // Icon can't be loaded from cachingLogic, which implies alternative icon was loaded
         // (e.g. fallback icon, default icon). So we drop here since there's no point in caching
         // an empty entry.
-        if (entry.bitmap.isNullOrLowRes()) return;
+        if (entry.icon == null) return;
         entry.title = cachingLogic.getLabel(object);
         entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
         if (cachingLogic.addToMemCache()) mCache.put(key, entry);
 
-        ContentValues values = newContentValues(entry.bitmap, entry.title.toString(),
+        ContentValues values = newContentValues(entry, entry.title.toString(),
                 componentName.getPackageName(), cachingLogic.getKeywords(object, mLocaleList));
         addIconToDB(values, componentName, info, userSerial);
     }
@@ -303,8 +300,8 @@
         return mDefaultIcons.get(user);
     }
 
-    public boolean isDefaultIcon(BitmapInfo icon, UserHandle user) {
-        return getDefaultIcon(user).icon == icon.icon;
+    public boolean isDefaultIcon(Bitmap icon, UserHandle user) {
+        return getDefaultIcon(user).icon == icon;
     }
 
     /**
@@ -318,7 +315,7 @@
         assertWorkerThread();
         ComponentKey cacheKey = new ComponentKey(componentName, user);
         CacheEntry entry = mCache.get(cacheKey);
-        if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) {
+        if (entry == null || (entry.isLowRes() && !useLowResIcon)) {
             entry = new CacheEntry();
             if (cachingLogic.addToMemCache()) {
                 mCache.put(cacheKey, entry);
@@ -333,7 +330,7 @@
                 providerFetchedOnce = true;
 
                 if (object != null) {
-                    entry.bitmap = cachingLogic.loadIcon(mContext, object);
+                    cachingLogic.loadIcon(mContext, object, entry);
                 } else {
                     if (usePackageIcon) {
                         CacheEntry packageEntry = getEntryForPackageLocked(
@@ -341,15 +338,15 @@
                         if (packageEntry != null) {
                             if (DEBUG) Log.d(TAG, "using package default icon for " +
                                     componentName.toShortString());
-                            entry.bitmap = packageEntry.bitmap;
+                            packageEntry.applyTo(entry);
                             entry.title = packageEntry.title;
                             entry.contentDescription = packageEntry.contentDescription;
                         }
                     }
-                    if (entry.bitmap == null) {
+                    if (entry.icon == null) {
                         if (DEBUG) Log.d(TAG, "using default icon for " +
                                 componentName.toShortString());
-                        entry.bitmap = getDefaultIcon(user);
+                        getDefaultIcon(user).applyTo(entry);
                     }
                 }
             }
@@ -393,10 +390,10 @@
         }
         if (icon != null) {
             BaseIconFactory li = getIconFactory();
-            entry.bitmap = li.createIconBitmap(icon);
+            li.createIconBitmap(icon).applyTo(entry);
             li.close();
         }
-        if (!TextUtils.isEmpty(title) && entry.bitmap.icon != null) {
+        if (!TextUtils.isEmpty(title) && entry.icon != null) {
             mCache.put(cacheKey, entry);
         }
     }
@@ -416,7 +413,7 @@
         ComponentKey cacheKey = getPackageKey(packageName, user);
         CacheEntry entry = mCache.get(cacheKey);
 
-        if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) {
+        if (entry == null || (entry.isLowRes() && !useLowResIcon)) {
             entry = new CacheEntry();
             boolean entryUpdated = true;
 
@@ -441,8 +438,8 @@
 
                     entry.title = appInfo.loadLabel(mPackageManager);
                     entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
-                    entry.bitmap = BitmapInfo.of(
-                            useLowResIcon ? LOW_RES_ICON : iconInfo.icon, iconInfo.color);
+                    entry.icon = useLowResIcon ? LOW_RES_ICON : iconInfo.icon;
+                    entry.color = iconInfo.color;
 
                     // Add the icon in the DB here, since these do not get written during
                     // package updates.
@@ -464,7 +461,7 @@
         return entry;
     }
 
-    protected boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
+    private boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
         Cursor c = null;
         try {
             c = mIconDb.query(
@@ -475,7 +472,7 @@
                             Long.toString(getSerialNumberForUser(cacheKey.user))});
             if (c.moveToNext()) {
                 // Set the alpha to be 255, so that we never have a wrong color
-                entry.bitmap = BitmapInfo.of(LOW_RES_ICON, setColorAlphaBound(c.getInt(0), 255));
+                entry.color = setColorAlphaBound(c.getInt(0), 255);
                 entry.title = c.getString(1);
                 if (entry.title == null) {
                     entry.title = "";
@@ -485,12 +482,13 @@
                             entry.title, cacheKey.user);
                 }
 
-                if (!lowRes) {
+                if (lowRes) {
+                    entry.icon = LOW_RES_ICON;
+                } else {
                     byte[] data = c.getBlob(2);
                     try {
-                        entry.bitmap = BitmapInfo.of(
-                                BitmapFactory.decodeByteArray(data, 0, data.length, mDecodeOptions),
-                                entry.bitmap.color);
+                        entry.icon = BitmapFactory.decodeByteArray(data, 0, data.length,
+                                mDecodeOptions);
                     } catch (Exception e) { }
                 }
                 return true;
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
index a89ede7..3aa783a 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
@@ -21,7 +21,6 @@
 import android.os.LocaleList;
 import android.os.UserHandle;
 
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.icons.BitmapInfo;
@@ -34,8 +33,7 @@
 
     CharSequence getLabel(T object);
 
-    @NonNull
-    BitmapInfo loadIcon(Context context, T object);
+    void loadIcon(Context context, T object, BitmapInfo target);
 
     /**
      * Provides a option list of keywords to associate with this object
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
index bcdbce5..d0db157 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
@@ -171,8 +171,7 @@
                 long updateTime = c.getLong(indexLastUpdate);
                 int version = c.getInt(indexVersion);
                 T app = componentMap.remove(component);
-                if (version == info.versionCode
-                        && updateTime == cachingLogic.getLastUpdatedTime(app, info)
+                if (version == info.versionCode && updateTime == info.lastUpdateTime
                         && TextUtils.equals(c.getString(systemStateIndex),
                                 mIconCache.getIconSystemState(info.packageName))) {
 
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
deleted file mode 100644
index 60afddb..0000000
--- a/quickstep/AndroidManifest-launcher.xml
+++ /dev/null
@@ -1,70 +0,0 @@
-<?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/res/drawable/predicted_icon_background.xml b/quickstep/recents_ui_overrides/res/drawable/predicted_icon_background.xml
deleted file mode 100644
index cfc6d48..0000000
--- a/quickstep/recents_ui_overrides/res/drawable/predicted_icon_background.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
-    android:inset="@dimen/predicted_icon_background_inset">
-    <shape>
-        <solid android:color="?attr/folderFillColor" />
-        <corners android:radius="@dimen/predicted_icon_background_corner_radius" />
-    </shape>
-</inset>
diff --git a/quickstep/recents_ui_overrides/res/values/dimens.xml b/quickstep/recents_ui_overrides/res/values/dimens.xml
index ee672d4..863a8ba 100644
--- a/quickstep/recents_ui_overrides/res/values/dimens.xml
+++ b/quickstep/recents_ui_overrides/res/values/dimens.xml
@@ -28,9 +28,4 @@
     <dimen name="swipe_up_fling_min_visible_change">18dp</dimen>
     <dimen name="swipe_up_y_overshoot">10dp</dimen>
     <dimen name="swipe_up_max_workspace_trans_y">-60dp</dimen>
-
-    <!-- Predicted icon related -->
-    <dimen name="predicted_icon_background_corner_radius">15dp</dimen>
-    <dimen name="predicted_icon_background_inset">8dp</dimen>
-
 </resources>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
deleted file mode 100644
index 424333c..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
+++ /dev/null
@@ -1,342 +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;
-
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-
-import android.animation.Animator;
-import android.animation.ObjectAnimator;
-import android.app.prediction.AppPredictionContext;
-import android.app.prediction.AppPredictionManager;
-import android.app.prediction.AppPredictor;
-import android.app.prediction.AppTarget;
-import android.content.ComponentName;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.allapps.AllAppsStore;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.appprediction.ComponentKeyMapper;
-import com.android.launcher3.appprediction.DynamicItemCache;
-import com.android.launcher3.dragndrop.DragController;
-import com.android.launcher3.dragndrop.DragOptions;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.popup.PopupContainerWithArrow;
-import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.touch.ItemLongClickListener;
-import com.android.launcher3.uioverrides.QuickstepLauncher;
-import com.android.launcher3.util.ComponentKey;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Provides prediction ability for the hotseat. Fills gaps in hotseat with predicted items, allows
- * pinning of predicted apps and manages replacement of predicted apps with user drag.
- */
-public class HotseatPredictionController implements DragController.DragListener,
-        View.OnAttachStateChangeListener, SystemShortcut.Factory<QuickstepLauncher>,
-        InvariantDeviceProfile.OnIDPChangeListener, AllAppsStore.OnUpdateListener,
-        IconCache.ItemInfoUpdateReceiver {
-
-    private static final String TAG = "PredictiveHotseat";
-    private static final boolean DEBUG = false;
-
-    private static final String PREDICTION_CLIENT = "hotseat";
-
-    private boolean mDragStarted = false;
-    private int mHotSeatItemsCount;
-
-    private Launcher mLauncher;
-    private Hotseat mHotseat;
-
-    private List<ComponentKeyMapper> mComponentKeyMappers = new ArrayList<>();
-
-    private DynamicItemCache mDynamicItemCache;
-
-    private AppPredictor mAppPredictor;
-    private AllAppsStore mAllAppsStore;
-
-    public HotseatPredictionController(Launcher launcher) {
-        mLauncher = launcher;
-        mHotseat = launcher.getHotseat();
-        mAllAppsStore = mLauncher.getAppsView().getAppsStore();
-        mAllAppsStore.addUpdateListener(this);
-        mDynamicItemCache = new DynamicItemCache(mLauncher, () -> fillGapsWithPrediction(false));
-        mHotSeatItemsCount = mLauncher.getDeviceProfile().inv.numHotseatIcons;
-        launcher.getDeviceProfile().inv.addOnChangeListener(this);
-        mHotseat.addOnAttachStateChangeListener(this);
-        createPredictor();
-    }
-
-    @Override
-    public void onViewAttachedToWindow(View view) {
-        mLauncher.getDragController().addDragListener(this);
-    }
-
-    @Override
-    public void onViewDetachedFromWindow(View view) {
-        mLauncher.getDragController().removeDragListener(this);
-    }
-
-    /**
-     * Fills gaps in the hotseat with predictions
-     */
-    public void fillGapsWithPrediction(boolean animate) {
-        if (mDragStarted) {
-            return;
-        }
-        List<WorkspaceItemInfo> predictedApps = mapToWorkspaceItemInfo(mComponentKeyMappers);
-        int predictionIndex = 0;
-        ArrayList<ItemInfo> newItemsToAdd = new ArrayList<>();
-        for (int rank = 0; rank < mHotSeatItemsCount; rank++) {
-            View child = mHotseat.getChildAt(
-                    mHotseat.getCellXFromOrder(rank),
-                    mHotseat.getCellYFromOrder(rank));
-
-            if (child != null && !isPredictedIcon(child)) {
-                continue;
-            }
-            if (predictedApps.size() <= predictionIndex) {
-                // Remove predicted apps from the past
-                if (isPredictedIcon(child)) {
-                    mHotseat.removeView(child);
-                }
-                continue;
-            }
-
-            WorkspaceItemInfo predictedItem = predictedApps.get(predictionIndex++);
-            if (isPredictedIcon(child)) {
-                BubbleTextView icon = (BubbleTextView) child;
-                icon.applyFromWorkspaceItem(predictedItem);
-            } else {
-                newItemsToAdd.add(predictedItem);
-            }
-            preparePredictionInfo(predictedItem, rank);
-        }
-        mLauncher.bindItems(newItemsToAdd, animate);
-        for (BubbleTextView icon : getPredictedIcons()) {
-            icon.verifyHighRes();
-            icon.setOnLongClickListener((v) -> {
-                PopupContainerWithArrow.showForIcon((BubbleTextView) v);
-                return true;
-            });
-            icon.setBackgroundResource(R.drawable.predicted_icon_background);
-        }
-    }
-
-    /**
-     * Unregisters callbacks and frees resources
-     */
-    public void destroy() {
-        mAllAppsStore.removeUpdateListener(this);
-        mLauncher.getDeviceProfile().inv.removeOnChangeListener(this);
-        mHotseat.removeOnAttachStateChangeListener(this);
-        if (mAppPredictor != null) {
-            mAppPredictor.destroy();
-        }
-    }
-
-    private void createPredictor() {
-        AppPredictionManager apm = mLauncher.getSystemService(AppPredictionManager.class);
-        if (apm == null) {
-            return;
-        }
-        if (mAppPredictor != null) {
-            mAppPredictor.destroy();
-        }
-        mAppPredictor = apm.createAppPredictionSession(
-                new AppPredictionContext.Builder(mLauncher)
-                        .setUiSurface(PREDICTION_CLIENT)
-                        .setPredictedTargetCount(mHotSeatItemsCount)
-                        .build());
-        mAppPredictor.registerPredictionUpdates(mLauncher.getMainExecutor(),
-                this::setPredictedApps);
-        mAppPredictor.requestPredictionUpdate();
-    }
-
-    private void setPredictedApps(List<AppTarget> appTargets) {
-        mComponentKeyMappers.clear();
-        for (AppTarget appTarget : appTargets) {
-            ComponentKey key;
-            if (appTarget.getShortcutInfo() != null) {
-                key = ShortcutKey.fromInfo(appTarget.getShortcutInfo());
-            } else {
-                key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
-                        appTarget.getClassName()), appTarget.getUser());
-            }
-            mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache));
-        }
-        updateDependencies();
-        fillGapsWithPrediction(false);
-    }
-
-    private void updateDependencies() {
-        mDynamicItemCache.updateDependencies(mComponentKeyMappers, mAllAppsStore, this,
-                mHotSeatItemsCount);
-    }
-
-    private void pinPrediction(ItemInfo info) {
-        BubbleTextView icon = (BubbleTextView) mHotseat.getChildAt(
-                mHotseat.getCellXFromOrder(info.rank),
-                mHotseat.getCellYFromOrder(info.rank));
-        if (icon == null) {
-            return;
-        }
-        WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo((WorkspaceItemInfo) info);
-        mLauncher.getModelWriter().addItemToDatabase(workspaceItemInfo,
-                LauncherSettings.Favorites.CONTAINER_HOTSEAT, workspaceItemInfo.screenId,
-                workspaceItemInfo.cellX, workspaceItemInfo.cellY);
-        ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 1, 0.8f, 1).start();
-        icon.reset();
-        icon.applyFromWorkspaceItem(workspaceItemInfo);
-        icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
-    }
-
-    private List<WorkspaceItemInfo> mapToWorkspaceItemInfo(
-            List<ComponentKeyMapper> components) {
-        AllAppsStore allAppsStore = mLauncher.getAppsView().getAppsStore();
-        if (allAppsStore.getApps().length == 0) {
-            return Collections.emptyList();
-        }
-
-        List<WorkspaceItemInfo> predictedApps = new ArrayList<>();
-        for (ComponentKeyMapper mapper : components) {
-            ItemInfoWithIcon info = mapper.getApp(allAppsStore);
-            if (info instanceof AppInfo) {
-                WorkspaceItemInfo predictedApp = new WorkspaceItemInfo((AppInfo) info);
-                predictedApps.add(predictedApp);
-            } else if (info instanceof WorkspaceItemInfo) {
-                predictedApps.add(new WorkspaceItemInfo((WorkspaceItemInfo) info));
-            } else {
-                if (DEBUG) {
-                    Log.e(TAG, "Predicted app not found: " + mapper);
-                }
-            }
-            // Stop at the number of hotseat items
-            if (predictedApps.size() == mHotSeatItemsCount) {
-                break;
-            }
-        }
-        return predictedApps;
-    }
-
-    private List<BubbleTextView> getPredictedIcons() {
-        List<BubbleTextView> icons = new ArrayList<>();
-        ViewGroup vg = mHotseat.getShortcutsAndWidgets();
-        for (int i = 0; i < vg.getChildCount(); i++) {
-            View child = vg.getChildAt(i);
-            if (isPredictedIcon(child)) {
-                icons.add((BubbleTextView) child);
-            }
-        }
-        return icons;
-    }
-
-    private void removePredictedApps(boolean animate) {
-        for (BubbleTextView icon : getPredictedIcons()) {
-            if (animate) {
-                icon.animate().scaleY(0).scaleX(0).setListener(new AnimationSuccessListener() {
-                    @Override
-                    public void onAnimationSuccess(Animator animator) {
-                        if (icon.getParent() != null) {
-                            mHotseat.removeView(icon);
-                        }
-                    }
-                });
-            } else {
-                if (icon.getParent() != null) {
-                    mHotseat.removeView(icon);
-                }
-            }
-        }
-    }
-
-    @Override
-    public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
-        removePredictedApps(true);
-        mDragStarted = true;
-    }
-
-    @Override
-    public void onDragEnd() {
-        if (!mDragStarted) {
-            return;
-        }
-        mDragStarted = false;
-        fillGapsWithPrediction(true);
-    }
-
-    @Nullable
-    @Override
-    public SystemShortcut<QuickstepLauncher> getShortcut(QuickstepLauncher activity,
-            ItemInfo itemInfo) {
-        if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
-            return null;
-        }
-        return new PinPrediction(activity, itemInfo);
-    }
-
-    private void preparePredictionInfo(WorkspaceItemInfo itemInfo, int rank) {
-        itemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
-        itemInfo.rank = rank;
-        itemInfo.cellX = rank;
-        itemInfo.cellY = mHotSeatItemsCount - rank - 1;
-        itemInfo.screenId = rank;
-    }
-
-    @Override
-    public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
-        this.mHotSeatItemsCount = profile.numHotseatIcons;
-        createPredictor();
-    }
-
-    @Override
-    public void onAppsUpdated() {
-        updateDependencies();
-        fillGapsWithPrediction(false);
-    }
-
-    @Override
-    public void reapplyItemInfo(ItemInfoWithIcon info) {
-
-    }
-
-    private class PinPrediction extends SystemShortcut<QuickstepLauncher> {
-
-        private PinPrediction(QuickstepLauncher target, ItemInfo itemInfo) {
-            super(R.drawable.ic_pin, R.string.pin_prediction, target,
-                    itemInfo);
-        }
-
-        @Override
-        public void onClick(View view) {
-            dismissTaskMenuView(mTarget);
-            pinPrediction(mItemInfo);
-        }
-    }
-
-    private static boolean isPredictedIcon(View view) {
-        return view instanceof BubbleTextView && view.getTag() instanceof WorkspaceItemInfo
-                && ((WorkspaceItemInfo) view.getTag()).container
-                == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index 6946508..d842484 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -17,10 +17,7 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
@@ -30,15 +27,12 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
 import android.content.Context;
 import android.view.View;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
-import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.SpringAnimationBuilder;
@@ -151,37 +145,8 @@
     @Override
     public Animator createStateElementAnimation(int index, float... values) {
         switch (index) {
-            case INDEX_SHELF_ANIM: {
-                AllAppsTransitionController aatc = mLauncher.getAllAppsController();
-                Animator springAnim = aatc.createSpringAnimation(values);
-
-                if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
-                    // Translate hotseat with the shelf until reaching overview.
-                    float overviewProgress = OVERVIEW.getVerticalProgress(mLauncher);
-                    ScaleAndTranslation sat = OVERVIEW.getHotseatScaleAndTranslation(mLauncher);
-                    float shiftRange = aatc.getShiftRange();
-                    if (values.length == 1) {
-                        values = new float[] {aatc.getProgress(), values[0]};
-                    }
-                    ValueAnimator hotseatAnim = ValueAnimator.ofFloat(values);
-                    hotseatAnim.addUpdateListener(anim -> {
-                        float progress = (Float) anim.getAnimatedValue();
-                        if (progress >= overviewProgress || mLauncher.isInState(BACKGROUND_APP)) {
-                            float hotseatShift = (progress - overviewProgress) * shiftRange;
-                            mLauncher.getHotseat().setTranslationY(hotseatShift + sat.translationY);
-                        }
-                    });
-                    hotseatAnim.setInterpolator(LINEAR);
-                    hotseatAnim.setDuration(springAnim.getDuration());
-
-                    AnimatorSet anim = new AnimatorSet();
-                    anim.play(hotseatAnim);
-                    anim.play(springAnim);
-                    return anim;
-                }
-
-                return springAnim;
-            }
+            case INDEX_SHELF_ANIM:
+                return mLauncher.getAllAppsController().createSpringAnimation(values);
             case INDEX_RECENTS_FADE_ANIM:
                 return ObjectAnimator.ofFloat(mLauncher.getOverviewPanel(),
                         RecentsView.CONTENT_ALPHA, values);
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java
new file mode 100644
index 0000000..76050d5
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherInitListenerEx.java
@@ -0,0 +1,34 @@
+/*
+ * 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 com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
+
+import java.util.function.BiPredicate;
+
+public class LauncherInitListenerEx extends LauncherInitListener {
+
+    public LauncherInitListenerEx(BiPredicate<Launcher, Boolean> onInitListener) {
+        super(onInitListener);
+    }
+
+    @Override
+    public boolean init(Launcher launcher, boolean alreadyOnHome) {
+        PredictionUiStateManager.INSTANCE.get(launcher).switchClient(Client.OVERVIEW);
+        return super.init(launcher, alreadyOnHome);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
index 0712285..b9f4147 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
@@ -18,6 +18,8 @@
 
 import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
 
+import android.content.Context;
+
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.allapps.AllAppsStore;
@@ -27,9 +29,11 @@
 public class ComponentKeyMapper {
 
     protected final ComponentKey componentKey;
+    private final Context mContext;
     private final DynamicItemCache mCache;
 
-    public ComponentKeyMapper(ComponentKey key, DynamicItemCache cache) {
+    public ComponentKeyMapper(Context context, ComponentKey key, DynamicItemCache cache) {
+        mContext = context;
         componentKey = key;
         mCache = cache;
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
index 38bb180..65e69b6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
@@ -18,7 +18,6 @@
 import static android.content.pm.PackageManager.MATCH_INSTANT;
 
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
 
 import android.content.Context;
 import android.content.Intent;
@@ -38,10 +37,8 @@
 import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.AppInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
@@ -75,7 +72,6 @@
     private final Handler mUiHandler;
     private final InstantAppResolver mInstantAppResolver;
     private final Runnable mOnUpdateCallback;
-    private final IconCache mIconCache;
 
     private final Map<ShortcutKey, WorkspaceItemInfo> mShortcuts;
     private final Map<String, InstantAppItemInfo> mInstantApps;
@@ -86,7 +82,6 @@
         mUiHandler = new Handler(Looper.getMainLooper(), this::handleUiMessage);
         mInstantAppResolver = InstantAppResolver.newInstance(context);
         mOnUpdateCallback = onUpdateCallback;
-        mIconCache = LauncherAppState.getInstance(mContext).getIconCache();
 
         mShortcuts = new HashMap<>();
         mInstantApps = new HashMap<>();
@@ -175,7 +170,7 @@
         if (!details.isEmpty()) {
             WorkspaceItemInfo si = new WorkspaceItemInfo(details.get(0), mContext);
             try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
-                si.bitmap = li.createShortcutIcon(details.get(0), true /* badged */, null);
+                si.applyFrom(li.createShortcutIcon(details.get(0), true /* badged */, null));
             } catch (Exception e) {
                 if (DEBUG) {
                     Log.e(TAG, "Error loading shortcut icon for " + shortcutKey.toString());
@@ -214,7 +209,7 @@
         InstantAppItemInfo info = new InstantAppItemInfo(intent, pkgName);
         IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
         iconCache.getTitleAndIcon(info, false);
-        if (info.bitmap.icon == null || iconCache.isDefaultIcon(info.bitmap, info.user)) {
+        if (info.iconBitmap == null || iconCache.isDefaultIcon(info.iconBitmap, info.user)) {
             return null;
         }
         return info;
@@ -245,35 +240,4 @@
     public WorkspaceItemInfo getShortcutInfo(ShortcutKey key) {
         return mShortcuts.get(key);
     }
-
-    /**
-     * requests and caches icons for app targets
-     */
-    public void updateDependencies(List<ComponentKeyMapper> componentKeyMappers,
-            AllAppsStore appsStore, IconCache.ItemInfoUpdateReceiver callback, int itemCount) {
-        List<String> instantAppsToLoad = new ArrayList<>();
-        List<ShortcutKey> shortcutsToLoad = new ArrayList<>();
-        int total = componentKeyMappers.size();
-        for (int i = 0, count = 0; i < total && count < itemCount; i++) {
-            ComponentKeyMapper mapper = componentKeyMappers.get(i);
-            // Update instant apps
-            if (COMPONENT_CLASS_MARKER.equals(mapper.getComponentClass())) {
-                instantAppsToLoad.add(mapper.getPackage());
-                count++;
-            } else if (mapper.getComponentKey() instanceof ShortcutKey) {
-                shortcutsToLoad.add((ShortcutKey) mapper.getComponentKey());
-                count++;
-            } else {
-                // Reload high res icon
-                AppInfo info = (AppInfo) mapper.getApp(appsStore);
-                if (info != null) {
-                    if (info.usingLowResIcon()) {
-                        mIconCache.updateIconInBackground(callback, info);
-                    }
-                    count++;
-                }
-            }
-        }
-        cacheItems(shortcutsToLoad, instantAppsToLoad);
-    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
index 8338c2e..1a59770 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
@@ -1,4 +1,4 @@
-/*
+/**
  * Copyright (C) 2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,12 +18,14 @@
 
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
 
 import android.app.prediction.AppPredictor;
 import android.app.prediction.AppTarget;
 import android.content.ComponentName;
 import android.content.Context;
 
+import com.android.launcher3.AppInfo;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
 import com.android.launcher3.ItemInfoWithIcon;
@@ -34,6 +36,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsStore.OnUpdateListener;
+import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ComponentKey;
@@ -236,7 +239,7 @@
                     key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
                             appTarget.getClassName()), appTarget.getUser());
                 }
-                state.apps.add(new ComponentKeyMapper(key, mDynamicItemCache));
+                state.apps.add(new ComponentKeyMapper(mContext, key, mDynamicItemCache));
             }
         }
         updateDependencies(state);
@@ -247,8 +250,33 @@
         if (!state.isEnabled || mAppsView == null) {
             return;
         }
-        mDynamicItemCache.updateDependencies(state.apps, mAppsView.getAppsStore(), this,
-                mMaxIconsPerRow);
+
+        IconCache iconCache = LauncherAppState.getInstance(mContext).getIconCache();
+        List<String> instantAppsToLoad = new ArrayList<>();
+        List<ShortcutKey> shortcutsToLoad = new ArrayList<>();
+        int total = state.apps.size();
+        for (int i = 0, count = 0; i < total && count < mMaxIconsPerRow; i++) {
+            ComponentKeyMapper mapper = state.apps.get(i);
+            // Update instant apps
+            if (COMPONENT_CLASS_MARKER.equals(mapper.getComponentClass())) {
+                instantAppsToLoad.add(mapper.getPackage());
+                count++;
+            } else if (mapper.getComponentKey() instanceof ShortcutKey) {
+                shortcutsToLoad.add((ShortcutKey) mapper.getComponentKey());
+                count++;
+            } else {
+                // Reload high res icon
+                AppInfo info = (AppInfo) mapper.getApp(mAppsView.getAppsStore());
+                if (info != null) {
+                    if (info.usingLowResIcon()) {
+                        // TODO: Update icon cache to support null callbacks.
+                        iconCache.updateIconInBackground(this, info);
+                    }
+                    count++;
+                }
+            }
+        }
+        mDynamicItemCache.cacheItems(shortcutsToLoad, instantAppsToLoad);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
similarity index 67%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
rename to quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
index 7dc5616..4c04b29 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
@@ -13,27 +13,26 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package com.android.launcher3.uioverrides;
 
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 
 import android.content.Context;
-import android.content.res.Configuration;
 import android.graphics.Rect;
-import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
 import android.view.Gravity;
 
-import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.HotseatPredictionController;
 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.config.FeatureFlags;
 import com.android.launcher3.graphics.RotationMode;
-import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.uioverrides.touchcontrollers.FlingAndHoldTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
 import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController;
@@ -49,20 +48,28 @@
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.TouchInteractionService;
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.views.RecentsView;
 
 import java.util.ArrayList;
-import java.util.stream.Stream;
 
-public class QuickstepLauncher extends BaseQuickstepLauncher {
+/**
+ * Provides recents-related {@link UiFactory} logic and classes.
+ */
+public abstract class RecentsUiFactory {
+
+    private static final String TAG = RecentsUiFactory.class.getSimpleName();
 
     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
         public void mapRect(int left, int top, int right, int bottom, Rect out) {
@@ -89,6 +96,7 @@
             }
         }
     };
+
     public static RotationMode ROTATION_SEASCAPE = new RotationMode(90) {
         @Override
         public void mapRect(int left, int top, int right, int bottom, Rect out) {
@@ -134,114 +142,83 @@
                     | horizontalGravity | verticalGravity;
         }
     };
-    private HotseatPredictionController mHotseatPredictionController;
 
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
-            mHotseatPredictionController = new HotseatPredictionController(this);
-        }
-    }
-
-    @Override
-    protected RotationMode getFakeRotationMode(DeviceProfile dp) {
+    public static RotationMode getRotationMode(DeviceProfile dp) {
         return !dp.isVerticalBarLayout() ? RotationMode.NORMAL
                 : (dp.isSeascape() ? ROTATION_SEASCAPE : ROTATION_LANDSCAPE);
     }
 
-    @Override
-    public void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-        onStateOrResumeChanged();
-    }
+    public static TouchController[] createTouchControllers(Launcher launcher) {
+        Mode mode = SysUINavigationMode.getMode(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();
-        }
-    }
-
-    @Override
-    public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
-        if (mHotseatPredictionController != null) {
-            return Stream.concat(super.getSupportedShortcuts(),
-                    Stream.of(mHotseatPredictionController));
+        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 {
-            return super.getSupportedShortcuts();
+            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()]);
+    }
+
+    /**
+     * 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);
+    }
+
+    /** Clears the swipe shared state for the current swipe gesture. */
+    public static void clearSwipeSharedState(Launcher launcher, boolean finishAnimation) {
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            launcher.<RecentsView>getOverviewPanel().switchToScreenshot(
+                    () -> TouchInteractionService.getSwipeSharedState().clearAllState(
+                            finishAnimation));
+        } else {
+            TouchInteractionService.getSwipeSharedState().clearAllState(finishAnimation);
         }
     }
 
     /**
      * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
+     *
+     * @param launcher the launcher activity
      */
-    private void onStateOrResumeChanged() {
-        LauncherState state = getStateManager().getState();
-        DeviceProfile profile = getDeviceProfile();
-        boolean visible = (state == NORMAL || state == OVERVIEW) && isUserActive()
+    public static void onLauncherStateOrResumeChanged(Launcher launcher) {
+        LauncherState state = launcher.getStateManager().getState();
+        DeviceProfile profile = launcher.getDeviceProfile();
+        boolean visible = (state == NORMAL || state == OVERVIEW) && launcher.isUserActive()
                 && !profile.isVerticalBarLayout();
-        UiThreadHelper.runAsyncCommand(this, SET_SHELF_HEIGHT, visible ? 1 : 0,
+        UiThreadHelper.runAsyncCommand(launcher, SET_SHELF_HEIGHT, visible ? 1 : 0,
                 profile.hotseatBarSizePx);
         if (state == NORMAL) {
-            ((RecentsView) getOverviewPanel()).setSwipeDownShouldLaunchApp(false);
+            launcher.<RecentsView>getOverviewPanel().setSwipeDownShouldLaunchApp(false);
         }
     }
 
-    @Override
-    public void finishBindingItems(int pageBoundFirst) {
-        super.finishBindingItems(pageBoundFirst);
-        if (mHotseatPredictionController != null) {
-            mHotseatPredictionController.fillGapsWithPrediction(false);
-        }
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-        if (mHotseatPredictionController != null) {
-            mHotseatPredictionController.destroy();
-        }
-    }
-
-    @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/states/BackgroundAppState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index bb66ae1..e4e60a0 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -69,7 +69,7 @@
             return super.getOverviewScaleAndTranslation(launcher);
         }
         TaskView dummyTask;
-        if (recentsView.getCurrentPage() >= recentsView.getTaskViewStartIndex()) {
+        if (recentsView.getCurrentPage() >= 0) {
             if (recentsView.getCurrentPage() <= taskCount - 1) {
                 dummyTask = recentsView.getCurrentPageTaskView();
             } else {
@@ -98,7 +98,7 @@
         if ((getVisibleElements(launcher) & HOTSEAT_ICONS) != 0) {
             // Translate hotseat offscreen if we show it in overview.
             ScaleAndTranslation scaleAndTranslation = super.getHotseatScaleAndTranslation(launcher);
-            scaleAndTranslation.translationY += LayoutUtils.getShelfTrackingDistance(launcher,
+            scaleAndTranslation.translationY = LayoutUtils.getShelfTrackingDistance(launcher,
                     launcher.getDeviceProfile());
             return scaleAndTranslation;
         }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 25eaab1..93d4de1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -32,6 +32,7 @@
 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
 import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
 
+import android.content.Context;
 import android.graphics.Rect;
 import android.view.View;
 
@@ -46,7 +47,6 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 
@@ -91,19 +91,8 @@
     @Override
     public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) {
         if ((getVisibleElements(launcher) & HOTSEAT_ICONS) != 0) {
-            DeviceProfile dp = launcher.getDeviceProfile();
-            if (dp.allAppsIconSizePx >= dp.iconSizePx) {
-                return new ScaleAndTranslation(1, 0, 0);
-            } else {
-                float scale = ((float) dp.allAppsIconSizePx) / dp.iconSizePx;
-                // Distance between the screen center (which is the pivotY for hotseat) and the
-                // bottom of the hotseat (which we want to preserve)
-                float distanceFromBottom = dp.heightPx / 2 - dp.hotseatBarBottomPaddingPx;
-                // On scaling, the bottom edge is moved closer to the pivotY. We move the
-                // hotseat back down so that the bottom edge's position is preserved.
-                float translationY = distanceFromBottom * (1 - scale);
-                return new ScaleAndTranslation(scale, 0, translationY);
-            }
+            // If the hotseat icons are visible in overview, keep them in their normal position.
+            return super.getWorkspaceScaleAndTranslation(launcher);
         }
         return getWorkspaceScaleAndTranslation(launcher);
     }
@@ -171,7 +160,15 @@
     }
 
     public static float getDefaultSwipeHeight(Launcher launcher) {
-        return LayoutUtils.getDefaultSwipeHeight(launcher, launcher.getDeviceProfile());
+        return getDefaultSwipeHeight(launcher, launcher.getDeviceProfile());
+    }
+
+    public static float getDefaultSwipeHeight(Context context, DeviceProfile dp) {
+        float swipeHeight = dp.allAppsCellHeightPx - dp.allAppsIconTextSizePx;
+        if (SysUINavigationMode.getMode(context) == SysUINavigationMode.Mode.NO_BUTTON) {
+            swipeHeight -= dp.getInsets().bottom;
+        }
+        return swipeHeight;
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
index 7b4bb02..6c9f46f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
@@ -20,14 +20,13 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.GestureState;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 
 /**
  * State to indicate we are about to launch a recent task. Note that this state is only used when
- * quick switching from launcher; quick switching from an app uses LauncherSwipeHandler.
- * @see GestureState.GestureEndTarget#NEW_TASK
+ * quick switching from launcher; quick switching from an app uses WindowTransformSwipeHelper.
+ * @see com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget#NEW_TASK
  */
 public class QuickSwitchState extends BackgroundAppState {
 
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 626292e..ee2e951 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,7 +51,6 @@
 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;
 
@@ -107,7 +106,8 @@
                     }
                 });
                 mPeekAnim.start();
-                VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
+                recentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
+                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
 
                 mLauncher.getDragLayer().getScrim().animateToSysuiMultiplier(isPaused ? 0 : 1,
                         peekDuration, 0);
@@ -173,7 +173,7 @@
     }
 
     @Override
-    public void onDragEnd(float velocity) {
+    public void onDragEnd(float velocity, boolean fling) {
         if (mMotionPauseDetector.isPaused() && handlingOverviewAnim()) {
             if (mPeekAnim != null) {
                 mPeekAnim.cancel();
@@ -196,7 +196,7 @@
             });
             overviewAnim.start();
         } else {
-            super.onDragEnd(velocity);
+            super.onDragEnd(velocity, fling);
         }
 
         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 ad4a343..d66af1a 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
@@ -22,9 +22,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.touch.AbstractStateChangeTouchController.SUCCESS_TRANSITION_PROGRESS;
-import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -45,25 +43,21 @@
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
+import com.android.launcher3.touch.SwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.TouchController;
-import com.android.quickstep.util.AssistantUtilities;
 import com.android.quickstep.views.RecentsView;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
 
 /**
  * Handles swiping up on the nav bar to go home from launcher, e.g. overview or all apps.
  */
-public class NavBarToHomeTouchController implements TouchController,
-        SingleAxisSwipeDetector.Listener {
+public class NavBarToHomeTouchController implements TouchController, SwipeDetector.Listener {
 
     private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL_3;
 
     private final Launcher mLauncher;
-    private final SingleAxisSwipeDetector mSwipeDetector;
+    private final SwipeDetector mSwipeDetector;
     private final float mPullbackDistance;
 
     private boolean mNoIntercept;
@@ -73,8 +67,7 @@
 
     public NavBarToHomeTouchController(Launcher launcher) {
         mLauncher = launcher;
-        mSwipeDetector = new SingleAxisSwipeDetector(mLauncher, this,
-                SingleAxisSwipeDetector.VERTICAL);
+        mSwipeDetector = new SwipeDetector(mLauncher, this, SwipeDetector.VERTICAL);
         mPullbackDistance = mLauncher.getResources().getDimension(R.dimen.home_pullback_distance);
     }
 
@@ -86,8 +79,7 @@
             if (mNoIntercept) {
                 return false;
             }
-            mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_POSITIVE,
-                    false /* ignoreSlop */);
+            mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
         }
 
         if (mNoIntercept) {
@@ -109,10 +101,6 @@
         if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
             return true;
         }
-        if (FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
-                && AssistantUtilities.isExcludedAssistantRunning()) {
-            return true;
-        }
         return false;
     }
 
@@ -139,13 +127,8 @@
             if (!recentsView.isRtl()) {
                 pullbackDist = -pullbackDist;
             }
-            ObjectAnimator pullback = ObjectAnimator.ofFloat(recentsView, TRANSLATION_X,
-                    pullbackDist);
+            Animator pullback = ObjectAnimator.ofFloat(recentsView, TRANSLATION_X, pullbackDist);
             pullback.setInterpolator(PULLBACK_INTERPOLATOR);
-            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-                pullback.addUpdateListener(
-                        valueAnimator -> recentsView.redrawLiveTile(false /* mightNeedToRefill */));
-            }
             anim.play(pullback);
         } else if (mStartState == ALL_APPS) {
             AnimatorSetBuilder builder = new AnimatorSetBuilder();
@@ -190,19 +173,13 @@
     }
 
     @Override
-    public void onDragEnd(float velocity) {
-        boolean fling = mSwipeDetector.isFling(velocity);
+    public void onDragEnd(float velocity, boolean fling) {
         final int logAction = fling ? Touch.FLING : Touch.SWIPE;
         float progress = mCurrentAnimation.getProgressFraction();
         float interpolatedProgress = PULLBACK_INTERPOLATOR.getInterpolation(progress);
         boolean success = interpolatedProgress >= SUCCESS_TRANSITION_PROGRESS
                 || (velocity < 0 && fling);
         if (success) {
-            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-                RecentsView recentsView = mLauncher.getOverviewPanel();
-                recentsView.switchToScreenshot(null,
-                        () -> recentsView.finishRecentsAnimation(true /* toRecents */, null));
-            }
             mLauncher.getStateManager().goToState(mEndState, true,
                     () -> onSwipeInteractionCompleted(mEndState));
             if (mStartState != mEndState) {
@@ -213,8 +190,6 @@
                 AbstractFloatingView.closeAllOpenViews(mLauncher);
                 logStateChange(topOpenView.getLogContainerType(), logAction);
             }
-            ActivityManagerWrapper.getInstance()
-                    .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
         } else {
             // Quickly return to the state we came from (we didn't move far).
             ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
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 912be98..5c3b55d 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
@@ -30,7 +30,6 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
-import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.view.MotionEvent;
@@ -43,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.SingleAxisSwipeDetector;
+import com.android.launcher3.touch.SwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.quickstep.SysUINavigationMode;
@@ -51,7 +50,6 @@
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
 
 /**
  * Handles quick switching to a recent task from the home screen.
@@ -61,10 +59,10 @@
     private @Nullable TaskView mTaskToLaunch;
 
     public QuickSwitchTouchController(Launcher launcher) {
-        this(launcher, SingleAxisSwipeDetector.HORIZONTAL);
+        this(launcher, SwipeDetector.HORIZONTAL);
     }
 
-    protected QuickSwitchTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) {
+    protected QuickSwitchTouchController(Launcher l, SwipeDetector.Direction dir) {
         super(l, dir);
     }
 
@@ -96,8 +94,6 @@
         super.onDragStart(start);
         mStartContainerType = LauncherLogProto.ContainerType.NAVBAR;
         mTaskToLaunch = mLauncher.<RecentsView>getOverviewPanel().getTaskViewAt(0);
-        ActivityManagerWrapper.getInstance()
-                .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
     }
 
     @Override
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 ad02de1..00e4f58 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,9 +19,6 @@
 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;
@@ -35,8 +32,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.touch.BaseSwipeDetector;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
+import com.android.launcher3.touch.SwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.FlingBlockCheck;
 import com.android.launcher3.util.PendingAnimation;
@@ -50,14 +46,15 @@
  * Touch controller for handling task view card swipes
  */
 public abstract class TaskViewTouchController<T extends BaseDraggingActivity>
-        extends AnimatorListenerAdapter implements TouchController,
-        SingleAxisSwipeDetector.Listener {
+        extends AnimatorListenerAdapter implements TouchController, SwipeDetector.Listener {
+
+    private static final String TAG = "OverviewSwipeController";
 
     // 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 SingleAxisSwipeDetector mDetector;
+    private final SwipeDetector mDetector;
     private final RecentsView mRecentsView;
     private final int[] mTempCords = new int[2];
 
@@ -77,7 +74,7 @@
     public TaskViewTouchController(T activity) {
         mActivity = activity;
         mRecentsView = activity.getOverviewPanel();
-        mDetector = new SingleAxisSwipeDetector(activity, this, SingleAxisSwipeDetector.VERTICAL);
+        mDetector = new SwipeDetector(activity, this, SwipeDetector.VERTICAL);
     }
 
     private boolean canInterceptTouch() {
@@ -116,7 +113,7 @@
             int directionsToDetectScroll = 0;
             boolean ignoreSlopWhenSettling = false;
             if (mCurrentAnimation != null) {
-                directionsToDetectScroll = DIRECTION_BOTH;
+                directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
                 ignoreSlopWhenSettling = true;
             } else {
                 mTaskBeingDragged = null;
@@ -129,12 +126,12 @@
                         if (!SysUINavigationMode.getMode(mActivity).hasGestures) {
                             // Don't allow swipe down to open if we don't support swipe up
                             // to enter overview.
-                            directionsToDetectScroll = DIRECTION_POSITIVE;
+                            directionsToDetectScroll = SwipeDetector.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()
-                                    ? DIRECTION_BOTH : DIRECTION_POSITIVE;
+                                    ? SwipeDetector.DIRECTION_BOTH : SwipeDetector.DIRECTION_POSITIVE;
                         }
                         break;
                     }
@@ -168,8 +165,8 @@
             return;
         }
         int scrollDirections = mDetector.getScrollDirections();
-        if (goingUp && ((scrollDirections & DIRECTION_POSITIVE) == 0)
-                || !goingUp && ((scrollDirections & DIRECTION_NEGATIVE) == 0)) {
+        if (goingUp && ((scrollDirections & SwipeDetector.DIRECTION_POSITIVE) == 0)
+                || !goingUp && ((scrollDirections & SwipeDetector.DIRECTION_NEGATIVE) == 0)) {
             // Trying to re-init in an unsupported direction.
             return;
         }
@@ -246,8 +243,7 @@
     }
 
     @Override
-    public void onDragEnd(float velocity) {
-        boolean fling = mDetector.isFling(velocity);
+    public void onDragEnd(float velocity, boolean fling) {
         final boolean goingToEnd;
         final int logAction;
         boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
@@ -264,7 +260,7 @@
             logAction = Touch.SWIPE;
             goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS;
         }
-        long animationDuration = BaseSwipeDetector.calculateDuration(
+        long animationDuration = SwipeDetector.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 0ed5291..f1e4041 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.SingleAxisSwipeDetector;
+import com.android.launcher3.touch.SwipeDetector;
 
 public class TransposedQuickSwitchTouchController extends QuickSwitchTouchController {
 
     public TransposedQuickSwitchTouchController(Launcher launcher) {
-        super(launcher, SingleAxisSwipeDetector.VERTICAL);
+        super(launcher, SwipeDetector.VERTICAL);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
index 59b117f..8a11ac8 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -70,7 +70,7 @@
         activity.<RecentsView>getOverviewPanel().showCurrentTask(mTargetTaskId);
         AbstractFloatingView.closeAllOpenViews(activity, wasVisible);
         BaseActivityInterface.AnimationFactory factory =
-                mHelper.prepareRecentsUI(wasVisible,
+                mHelper.prepareRecentsUI(activity, wasVisible,
                 false /* animate activity */, (controller) -> {
                     controller.dispatchOnStart();
                     ValueAnimator anim = controller.getAnimationPlayer()
@@ -102,7 +102,7 @@
         anim.addListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationSuccess(Animator animator) {
-                mHelper.onSwipeUpToRecentsComplete();
+                mHelper.onSwipeUpToRecentsComplete(mActivity);
                 if (mRecentsView != null) {
                     mRecentsView.animateUpRunningTaskIconScale();
                 }
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 4f50e33..e1e994c 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,20 @@
  */
 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.VibratorWrapper.OVERVIEW_HAPTIC;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 
 import android.animation.Animator;
 import android.annotation.TargetApi;
+import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Point;
@@ -31,6 +36,11 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 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;
@@ -45,9 +55,9 @@
 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;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.AppWindowAnimationHelper;
@@ -86,14 +96,17 @@
     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;
-    protected final InputConsumerController mInputConsumer;
+    protected final RecentsModel mRecentsModel;
+    protected final int mRunningTaskId;
 
     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
@@ -101,6 +114,7 @@
     protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
 
     protected final ActivityInitListener mActivityInitListener;
+    protected final InputConsumerController mInputConsumer;
 
     protected RecentsAnimationController mRecentsAnimationController;
     protected RecentsAnimationTargets mRecentsAnimationTargets;
@@ -115,30 +129,54 @@
 
     protected Runnable mGestureEndCallback;
 
+    protected final Handler mMainThreadHandler = MAIN_EXECUTOR.getHandler();
     protected MultiStateCallback mStateCallback;
 
     protected boolean mCanceled;
     protected int mFinishingRecentsAnimationForNewTaskId = -1;
 
-    protected BaseSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
-            GestureState gestureState, InputConsumerController inputConsumer) {
+    protected BaseSwipeUpHandler(Context context, GestureState gestureState,
+            OverviewComponentObserver overviewComponentObserver,
+            RecentsModel recentsModel, InputConsumerController inputConsumer, int runningTaskId) {
         mContext = context;
-        mDeviceState = deviceState;
-        mGestureState = gestureState;
+        mOverviewComponentObserver = overviewComponentObserver;
         mActivityInterface = gestureState.getActivityInterface();
+        mRecentsModel = recentsModel;
         mActivityInitListener =
                 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 setStateOnUiThread(int stateFlag) {
+        if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
+            mStateCallback.setState(stateFlag);
+        } else {
+            postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag));
+        }
+    }
+
     protected void performHapticFeedback() {
-        VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
+        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));
     }
 
     public Consumer<MotionEvent> getRecentsViewDispatcher(RotationMode rotationMode) {
@@ -208,14 +246,14 @@
                                 success -> {
                                     resultCallback.accept(success);
                                     if (!success) {
-                                        mActivityInterface.onLaunchTaskFailed();
+                                        mActivityInterface.onLaunchTaskFailed(mActivity);
                                         nextTask.notifyTaskLaunchFailed(TAG);
                                     } else {
-                                        mActivityInterface.onLaunchTaskSuccess();
+                                        mActivityInterface.onLaunchTaskSuccess(mActivity);
                                     }
-                                }, MAIN_EXECUTOR.getHandler());
+                                }, mMainThreadHandler);
                     }
-                    mStateCallback.setStateOnUiThread(successStateFlag);
+                    setStateOnUiThread(successStateFlag);
                 }
                 mCanceled = false;
                 mFinishingRecentsAnimationForNewTaskId = -1;
@@ -250,8 +288,7 @@
         mRecentsAnimationTargets = targets;
         DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext);
         final Rect overviewStackBounds;
-        RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(
-                mGestureState.getRunningTaskId());
+        RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(mRunningTaskId);
 
         if (targets.minimizedHomeBounds != null && runningTaskTarget != null) {
             overviewStackBounds = mActivityInterface
@@ -318,7 +355,7 @@
             mAppWindowAnimationHelper.updateHomeBounds(getStackBounds(dp));
         }
         mAppWindowAnimationHelper.updateTargetRect(TEMP_RECT);
-        if (mDeviceState.isFullyGesturalNavMode()) {
+        if (mMode == Mode.NO_BUTTON) {
             // We can drag all the way to the top of the screen.
             mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
         }
@@ -329,7 +366,7 @@
      */
     protected abstract boolean moveWindowWithRecentsScroll();
 
-    protected abstract boolean onActivityInit(Boolean alreadyOnHome);
+    protected abstract boolean onActivityInit(final T activity, Boolean alreadyOnHome);
 
     /**
      * Called to create a input proxy for the running task
@@ -357,13 +394,13 @@
     @UiThread
     public abstract void onGestureEnded(float endVelocity, PointF velocity, PointF downPos);
 
-    public abstract void onConsumerAboutToBeSwitched();
+    public abstract void onConsumerAboutToBeSwitched(SwipeSharedState sharedState);
 
     public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) { }
 
     public void initWhenReady() {
         // Preload the plan
-        RecentsModel.INSTANCE.get(mContext).getTasks(null);
+        mRecentsModel.getTasks(null);
 
         mActivityInitListener.register();
     }
@@ -480,8 +517,8 @@
 
     public interface Factory {
 
-        BaseSwipeUpHandler newHandler(GestureState gestureState, long touchTimeMs,
-                boolean continuingLastGesture, boolean isLikelyToStartNewTask);
+        BaseSwipeUpHandler newHandler(GestureState gestureState, RunningTaskInfo runningTask,
+                long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask);
     }
 
     protected interface RunningWindowAnim {
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 f889bc1..8deb835 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
@@ -30,7 +30,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
@@ -41,8 +40,8 @@
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
+import java.util.function.BiPredicate;
 import java.util.function.Consumer;
-import java.util.function.Predicate;
 
 /**
  * {@link BaseActivityInterface} for recents when the default launcher is different than the
@@ -55,7 +54,7 @@
     public FallbackActivityInterface() { }
 
     @Override
-    public void onTransitionCancelled(boolean activityVisible) {
+    public void onTransitionCancelled(RecentsActivity activity, boolean activityVisible) {
         // TODO:
     }
 
@@ -73,11 +72,7 @@
     }
 
     @Override
-    public void onSwipeUpToRecentsComplete() {
-        RecentsActivity activity = getCreatedActivity();
-        if (activity == null) {
-            return;
-        }
+    public void onSwipeUpToRecentsComplete(RecentsActivity activity) {
         RecentsView recentsView = activity.getOverviewPanel();
         recentsView.getClearAllButton().setVisibilityAlpha(1);
         recentsView.setDisallowScrollToClearAll(false);
@@ -92,8 +87,7 @@
 
     @NonNull
     @Override
-    public HomeAnimationFactory prepareHomeUI() {
-        RecentsActivity activity = getCreatedActivity();
+    public HomeAnimationFactory prepareHomeUI(RecentsActivity activity) {
         RecentsView recentsView = activity.getOverviewPanel();
 
         return new HomeAnimationFactory() {
@@ -124,9 +118,8 @@
     }
 
     @Override
-    public AnimationFactory prepareRecentsUI(boolean activityVisible,
+    public AnimationFactory prepareRecentsUI(RecentsActivity activity, boolean activityVisible,
             boolean animateActivity, Consumer<AnimatorPlaybackController> callback) {
-        RecentsActivity activity = getCreatedActivity();
         if (activityVisible) {
             return (transitionLength) -> { };
         }
@@ -183,9 +176,8 @@
 
     @Override
     public ActivityInitListener createActivityInitListener(
-            Predicate<Boolean> onInitListener) {
-        return new ActivityInitListener<>((activity, alreadyOnHome) ->
-                onInitListener.test(alreadyOnHome), RecentsActivity.ACTIVITY_TRACKER);
+            BiPredicate<RecentsActivity, Boolean> onInitListener) {
+        return new ActivityInitListener(onInitListener, RecentsActivity.ACTIVITY_TRACKER);
     }
 
     @Nullable
@@ -236,21 +228,13 @@
     }
 
     @Override
-    public void onLaunchTaskFailed() {
+    public void onLaunchTaskFailed(RecentsActivity activity) {
         // 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;
-        }
+    public void onLaunchTaskSuccess(RecentsActivity activity) {
         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 844152b..f6b3654 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -29,7 +29,7 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.INSTANT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.LauncherSwipeHandler.RECENTS_ATTACH_DURATION;
+import static com.android.quickstep.WindowTransformSwipeHandler.RECENTS_ATTACH_DURATION;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -47,23 +47,20 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
-import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherInitListener;
+import com.android.launcher3.LauncherInitListenerEx;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.AnimatorSetBuilder;
-import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.uioverrides.states.OverviewState;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.LayoutUtils;
-import com.android.quickstep.util.ShelfPeekAnim;
-import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
 import com.android.quickstep.util.StaggeredWorkspaceAnim;
 import com.android.quickstep.views.LauncherRecentsView;
 import com.android.quickstep.views.RecentsView;
@@ -72,8 +69,8 @@
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
+import java.util.function.BiPredicate;
 import java.util.function.Consumer;
-import java.util.function.Predicate;
 
 /**
  * {@link BaseActivityInterface} for the in-launcher recents.
@@ -95,65 +92,50 @@
     }
 
     @Override
-    public void onTransitionCancelled(boolean activityVisible) {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
-        LauncherState startState = launcher.getStateManager().getRestState();
-        launcher.getStateManager().goToState(startState, activityVisible);
+    public void onTransitionCancelled(Launcher activity, boolean activityVisible) {
+        LauncherState startState = activity.getStateManager().getRestState();
+        activity.getStateManager().goToState(startState, activityVisible);
     }
 
     @Override
-    public void onSwipeUpToRecentsComplete() {
+    public void onSwipeUpToRecentsComplete(Launcher activity) {
         // 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);
+        activity.getStateManager().reapplyState();
+        DiscoveryBounce.showForOverviewIfNeeded(activity);
     }
 
     @Override
-    public void onSwipeUpToHomeComplete() {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
+    public void onSwipeUpToHomeComplete(Launcher activity) {
         // 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.getStateManager().reapplyState();
+        activity.getStateManager().reapplyState();
         setLauncherHideBackArrow(false);
     }
 
     private void setLauncherHideBackArrow(boolean hideBackArrow) {
         Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
+        if (launcher != null) {
+            launcher.getRootView().setForceHideBackArrow(hideBackArrow);
         }
-        launcher.getRootView().setForceHideBackArrow(hideBackArrow);
     }
 
     @Override
     public void onAssistantVisibilityChanged(float visibility) {
         Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
+        if (launcher != null) {
+            launcher.onAssistantVisibilityChanged(visibility);
         }
-        launcher.onAssistantVisibilityChanged(visibility);
     }
 
     @NonNull
     @Override
-    public HomeAnimationFactory prepareHomeUI() {
-        Launcher launcher = getCreatedActivity();
-        final DeviceProfile dp = launcher.getDeviceProfile();
-        final RecentsView recentsView = launcher.getOverviewPanel();
+    public HomeAnimationFactory prepareHomeUI(Launcher activity) {
+        final DeviceProfile dp = activity.getDeviceProfile();
+        final RecentsView recentsView = activity.getOverviewPanel();
         final TaskView runningTaskView = recentsView.getRunningTaskView();
         final View workspaceView;
         if (runningTaskView != null && runningTaskView.getTask().key.getComponent() != null) {
-            workspaceView = launcher.getWorkspace().getFirstMatchForAppClose(
+            workspaceView = activity.getWorkspace().getFirstMatchForAppClose(
                     runningTaskView.getTask().key.getComponent().getPackageName(),
                     UserHandle.of(runningTaskView.getTask().key.userId));
         } else {
@@ -162,7 +144,7 @@
         final RectF iconLocation = new RectF();
         boolean canUseWorkspaceView = workspaceView != null && workspaceView.isAttachedToWindow();
         FloatingIconView floatingIconView = canUseWorkspaceView
-                ? FloatingIconView.getFloatingIconView(launcher, workspaceView,
+                ? FloatingIconView.getFloatingIconView(activity, workspaceView,
                         true /* hideOriginal */, iconLocation, false /* isOpening */)
                 : null;
         setLauncherHideBackArrow(true);
@@ -188,14 +170,14 @@
             public AnimatorPlaybackController createActivityAnimationToHome() {
                 // Return an empty APC here since we have an non-user controlled animation to home.
                 long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx);
-                return launcher.getStateManager().createAnimationToNewWorkspace(NORMAL, accuracy,
+                return activity.getStateManager().createAnimationToNewWorkspace(NORMAL, accuracy,
                         0 /* animComponents */);
             }
 
             @Override
             public void playAtomicAnimation(float velocity) {
                 // Setup workspace with 0 duration to prepare for our staggered animation.
-                LauncherStateManager stateManager = launcher.getStateManager();
+                LauncherStateManager stateManager = activity.getStateManager();
                 AnimatorSetBuilder builder = new AnimatorSetBuilder();
                 // setRecentsAttachedToAppWindow() will animate recents out.
                 builder.addFlag(AnimatorSetBuilder.FLAG_DONT_ANIMATE_OVERVIEW);
@@ -205,40 +187,39 @@
                 // Stop scrolling so that it doesn't interfere with the translation offscreen.
                 recentsView.getScroller().forceFinished(true);
 
-                new StaggeredWorkspaceAnim(launcher, workspaceView, velocity).start();
+                new StaggeredWorkspaceAnim(activity, workspaceView, velocity).start();
             }
         };
     }
 
     @Override
-    public AnimationFactory prepareRecentsUI(boolean activityVisible,
+    public AnimationFactory prepareRecentsUI(Launcher activity, boolean activityVisible,
             boolean animateActivity, Consumer<AnimatorPlaybackController> callback) {
-        BaseQuickstepLauncher launcher = getCreatedActivity();
-        final LauncherState startState = launcher.getStateManager().getState();
+        final LauncherState startState = activity.getStateManager().getState();
 
         LauncherState resetState = startState;
         if (startState.disableRestore) {
-            resetState = launcher.getStateManager().getRestState();
+            resetState = activity.getStateManager().getRestState();
         }
-        launcher.getStateManager().setRestState(resetState);
+        activity.getStateManager().setRestState(resetState);
 
         final LauncherState fromState = animateActivity ? BACKGROUND_APP : OVERVIEW;
-        launcher.getStateManager().goToState(fromState, false);
+        activity.getStateManager().goToState(fromState, false);
         // Since all apps is not visible, we can safely reset the scroll position.
         // This ensures then the next swipe up to all-apps starts from scroll 0.
-        launcher.getAppsView().reset(false /* animate */);
+        activity.getAppsView().reset(false /* animate */);
 
         return new AnimationFactory() {
-            private final ShelfPeekAnim mShelfAnim = launcher.getShelfPeekAnim();
+            private ShelfAnimState mShelfState;
             private boolean mIsAttachedToWindow;
 
             @Override
             public void createActivityInterface(long transitionLength) {
-                createActivityInterfaceInternal(launcher, fromState, transitionLength, callback);
+                createActivityInterfaceInternal(activity, fromState, transitionLength, callback);
                 // Creating the activity controller animation sometimes reapplies the launcher state
                 // (because we set the animation as the current state animation), so we reapply the
                 // attached state here as well to ensure recents is shown/hidden appropriately.
-                if (SysUINavigationMode.getMode(launcher) == Mode.NO_BUTTON) {
+                if (SysUINavigationMode.getMode(activity) == Mode.NO_BUTTON) {
                     setRecentsAttachedToAppWindow(mIsAttachedToWindow, false);
                 }
             }
@@ -252,13 +233,36 @@
 
             @Override
             public void onTransitionCancelled() {
-                launcher.getStateManager().goToState(startState, false /* animate */);
+                activity.getStateManager().goToState(startState, false /* animate */);
             }
 
             @Override
             public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator,
                     long duration) {
-                mShelfAnim.setShelfState(shelfState, interpolator, duration);
+                if (mShelfState == shelfState) {
+                    return;
+                }
+                mShelfState = shelfState;
+                activity.getStateManager().cancelStateElementAnimation(INDEX_SHELF_ANIM);
+                if (mShelfState == ShelfAnimState.CANCEL) {
+                    return;
+                }
+                float shelfHiddenProgress = BACKGROUND_APP.getVerticalProgress(activity);
+                float shelfOverviewProgress = OVERVIEW.getVerticalProgress(activity);
+                // Peek based on default overview progress so we can see hotseat if we're showing
+                // that instead of predictions in overview.
+                float defaultOverviewProgress = OverviewState.getDefaultVerticalProgress(activity);
+                float shelfPeekingProgress = shelfHiddenProgress
+                        - (shelfHiddenProgress - defaultOverviewProgress) * 0.25f;
+                float toProgress = mShelfState == ShelfAnimState.HIDE
+                        ? shelfHiddenProgress
+                        : mShelfState == ShelfAnimState.PEEK
+                                ? shelfPeekingProgress
+                                : shelfOverviewProgress;
+                Animator shelfAnim = activity.getStateManager()
+                        .createStateElementAnimation(INDEX_SHELF_ANIM, toProgress);
+                shelfAnim.setInterpolator(interpolator);
+                shelfAnim.setDuration(duration).start();
             }
 
             @Override
@@ -267,8 +271,8 @@
                     return;
                 }
                 mIsAttachedToWindow = attached;
-                LauncherRecentsView recentsView = launcher.getOverviewPanel();
-                Animator fadeAnim = launcher.getStateManager()
+                LauncherRecentsView recentsView = activity.getOverviewPanel();
+                Animator fadeAnim = activity.getStateManager()
                         .createStateElementAnimation(
                         INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
 
@@ -282,7 +286,7 @@
 
                     float fromTranslationX = attached ? offscreenX - scrollOffsetX : 0;
                     float toTranslationX = attached ? 0 : offscreenX - scrollOffsetX;
-                    launcher.getStateManager()
+                    activity.getStateManager()
                             .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
 
                     if (!recentsView.isShown() && animate) {
@@ -294,7 +298,7 @@
                     if (!animate) {
                         recentsView.setTranslationX(toTranslationX);
                     } else {
-                        launcher.getStateManager().createStateElementAnimation(
+                        activity.getStateManager().createStateElementAnimation(
                                 INDEX_RECENTS_TRANSLATE_X_ANIM,
                                 fromTranslationX, toTranslationX).start();
                     }
@@ -392,15 +396,15 @@
     }
 
     @Override
-    public ActivityInitListener createActivityInitListener(Predicate<Boolean> onInitListener) {
-        return new LauncherInitListener((activity, alreadyOnHome) ->
-                onInitListener.test(alreadyOnHome));
+    public ActivityInitListener createActivityInitListener(
+            BiPredicate<Launcher, Boolean> onInitListener) {
+        return new LauncherInitListenerEx(onInitListener);
     }
 
     @Nullable
     @Override
-    public BaseQuickstepLauncher getCreatedActivity() {
-        return BaseQuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity();
+    public Launcher getCreatedActivity() {
+        return Launcher.ACTIVITY_TRACKER.getCreatedActivity();
     }
 
     @Nullable
@@ -465,20 +469,12 @@
     }
 
     @Override
-    public void onLaunchTaskFailed() {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
+    public void onLaunchTaskFailed(Launcher launcher) {
         launcher.getStateManager().goToState(OVERVIEW);
     }
 
     @Override
-    public void onLaunchTaskSuccess() {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
+    public void onLaunchTaskSuccess(Launcher launcher) {
         launcher.getStateManager().moveToRestState();
     }
 
@@ -497,38 +493,22 @@
     }
 
     @Override
-    public void switchRunningTaskViewToScreenshot(ThumbnailData thumbnailData,
-            Runnable onFinishRunnable) {
+    public void switchToScreenshot(ThumbnailData thumbnailData, Runnable runnable) {
         Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
         RecentsView recentsView = launcher.getOverviewPanel();
         if (recentsView == null) {
-            if (onFinishRunnable != null) {
-                onFinishRunnable.run();
+            if (runnable != null) {
+                runnable.run();
             }
             return;
         }
-        recentsView.switchToScreenshot(thumbnailData, onFinishRunnable);
-    }
-
-    @Override
-    public void setOnDeferredActivityLaunchCallback(Runnable r) {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
+        TaskView taskView = recentsView.getRunningTaskView();
+        if (taskView != null) {
+            taskView.setShowScreenshot(true);
+            taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData);
+            ViewUtils.postDraw(taskView, runnable);
+        } else if (runnable != null) {
+            runnable.run();
         }
-        launcher.setOnDeferredActivityLaunchCallback(r);
-    }
-
-    @Override
-    public void updateOverviewPredictionState() {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
-        PredictionUiStateManager.INSTANCE.get(launcher).switchClient(
-                PredictionUiStateManager.Client.OVERVIEW);
     }
 }
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/MultiStateCallback.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/MultiStateCallback.java
new file mode 100644
index 0000000..357c9fc
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/MultiStateCallback.java
@@ -0,0 +1,137 @@
+/*
+ * 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.quickstep;
+
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.launcher3.config.FeatureFlags;
+
+import java.util.StringJoiner;
+import java.util.function.Consumer;
+
+/**
+ * Utility class to help manage multiple callbacks based on different states.
+ */
+public class MultiStateCallback {
+
+    private static final String TAG = "MultiStateCallback";
+    public static final boolean DEBUG_STATES = false;
+
+    private final SparseArray<Runnable> mCallbacks = new SparseArray<>();
+    private final SparseArray<Consumer<Boolean>> mStateChangeHandlers = new SparseArray<>();
+
+    private final String[] mStateNames;
+
+    public MultiStateCallback(String[] stateNames) {
+        mStateNames = DEBUG_STATES ? stateNames : null;
+    }
+
+    private int mState = 0;
+
+    /**
+     * Adds the provided state flags to the global state and executes any callbacks as a result.
+     */
+    public void setState(int stateFlag) {
+        if (DEBUG_STATES) {
+            Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding "
+                    + convertToFlagNames(stateFlag) + " to " + convertToFlagNames(mState));
+        }
+
+        int oldState = mState;
+        mState = mState | stateFlag;
+
+        int count = mCallbacks.size();
+        for (int i = 0; i < count; i++) {
+            int state = mCallbacks.keyAt(i);
+
+            if ((mState & state) == state) {
+                Runnable callback = mCallbacks.valueAt(i);
+                if (callback != null) {
+                    // Set the callback to null, so that it does not run again.
+                    mCallbacks.setValueAt(i, null);
+                    callback.run();
+                }
+            }
+        }
+        notifyStateChangeHandlers(oldState);
+    }
+
+    /**
+     * Adds the provided state flags to the global state and executes any change handlers
+     * as a result.
+     */
+    public void clearState(int stateFlag) {
+        if (DEBUG_STATES) {
+            Log.d(TAG, "[" + System.identityHashCode(this) + "] Removing "
+                    + convertToFlagNames(stateFlag) + " from " + convertToFlagNames(mState));
+        }
+
+        int oldState = mState;
+        mState = mState & ~stateFlag;
+        notifyStateChangeHandlers(oldState);
+    }
+
+    private void notifyStateChangeHandlers(int oldState) {
+        int count = mStateChangeHandlers.size();
+        for (int i = 0; i < count; i++) {
+            int state = mStateChangeHandlers.keyAt(i);
+            boolean wasOn = (state & oldState) == state;
+            boolean isOn = (state & mState) == state;
+
+            if (wasOn != isOn) {
+                mStateChangeHandlers.valueAt(i).accept(isOn);
+            }
+        }
+    }
+
+    /**
+     * Sets the callbacks to be run when the provided states are enabled.
+     * The callback is only run once.
+     */
+    public void addCallback(int stateMask, Runnable callback) {
+        if (FeatureFlags.IS_DOGFOOD_BUILD && mCallbacks.get(stateMask) != null) {
+            throw new IllegalStateException("Multiple callbacks on same state");
+        }
+        mCallbacks.put(stateMask, callback);
+    }
+
+    /**
+     * Sets the handler to be called when the provided states are enabled or disabled.
+     */
+    public void addChangeHandler(int stateMask, Consumer<Boolean> handler) {
+        mStateChangeHandlers.put(stateMask, handler);
+    }
+
+    public int getState() {
+        return mState;
+    }
+
+    public boolean hasStates(int stateMask) {
+        return (mState & stateMask) == stateMask;
+    }
+
+    private String convertToFlagNames(int flags) {
+        StringJoiner joiner = new StringJoiner(", ", "[", " (" + flags + ")]");
+        for (int i = 0; i < mStateNames.length; i++) {
+            if ((flags & (1 << i)) != 0) {
+                joiner.add(mStateNames[i]);
+            }
+        }
+        return joiner.toString();
+    }
+
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
index c4733bd..150c44d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
@@ -29,7 +29,6 @@
 
 import androidx.annotation.BinderThread;
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.util.ActivityInitListener;
@@ -204,8 +203,7 @@
             return false;
         }
 
-        private boolean onActivityReady(Boolean wasVisible) {
-            final T activity = mActivityInterface.getCreatedActivity();
+        private boolean onActivityReady(T activity, Boolean wasVisible) {
             if (!mUserEventLogged) {
                 activity.getUserEventDispatcher().logActionCommand(
                         LauncherLogProto.Action.Command.RECENTS_BUTTON,
@@ -213,10 +211,6 @@
                         LauncherLogProto.ContainerType.TASKSWITCHER);
                 mUserEventLogged = true;
             }
-
-            // Switch prediction client to overview
-            PredictionUiStateManager.INSTANCE.get(activity).switchClient(
-                    PredictionUiStateManager.Client.OVERVIEW);
             return mAnimationProvider.onActivityReady(activity, wasVisible);
         }
 
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 92c55da..ce533a6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -10,6 +10,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.testing.TestInformationHandler;
 import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.uioverrides.states.OverviewState;
 import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.views.RecentsView;
@@ -33,7 +34,7 @@
         switch (method) {
             case TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT: {
                 final float swipeHeight =
-                        LayoutUtils.getDefaultSwipeHeight(mContext, mDeviceProfile);
+                        OverviewState.getDefaultSwipeHeight(mContext, mDeviceProfile);
                 response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, (int) swipeHeight);
                 return response;
             }
@@ -111,6 +112,11 @@
 
     @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/SwipeSharedState.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java
new file mode 100644
index 0000000..cd8e1a4
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeSharedState.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.util.Log;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.Preconditions;
+import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
+
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+import java.io.PrintWriter;
+
+/**
+ * Utility class used to store state information shared across multiple transitions.
+ */
+public class SwipeSharedState implements RecentsAnimationListener {
+
+    private OverviewComponentObserver mOverviewComponentObserver;
+
+    private RecentsAnimationCallbacks mRecentsAnimationListener;
+    private RecentsAnimationController mLastRecentsAnimationController;
+    private RecentsAnimationTargets mLastAnimationTarget;
+
+    private boolean mLastAnimationCancelled = false;
+    private boolean mLastAnimationRunning = false;
+
+    public boolean canGestureBeContinued;
+    public boolean goingToLauncher;
+    public boolean recentsAnimationFinishInterrupted;
+    public int nextRunningTaskId = -1;
+    private int mLogId;
+
+    public void setOverviewComponentObserver(OverviewComponentObserver observer) {
+        mOverviewComponentObserver = observer;
+    }
+
+    @Override
+    public final void onRecentsAnimationStart(RecentsAnimationController controller,
+            RecentsAnimationTargets targets) {
+        mLastRecentsAnimationController = controller;
+        mLastAnimationTarget = targets;
+
+        mLastAnimationCancelled = false;
+        mLastAnimationRunning = true;
+    }
+
+    @Override
+    public final void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
+        if (thumbnailData != null) {
+            mOverviewComponentObserver.getActivityInterface().switchToScreenshot(thumbnailData,
+                    () -> {
+                        mLastRecentsAnimationController.cleanupScreenshot();
+                        clearAnimationState();
+                    });
+        } else {
+            clearAnimationState();
+        }
+    }
+
+    @Override
+    public final void onRecentsAnimationFinished(RecentsAnimationController controller) {
+        if (mLastRecentsAnimationController == controller) {
+            mLastAnimationRunning = false;
+        }
+    }
+
+    private void clearAnimationTarget() {
+        if (mLastAnimationTarget != null) {
+            mLastAnimationTarget.release();
+            mLastAnimationTarget = null;
+        }
+    }
+
+    private void clearAnimationState() {
+        clearAnimationTarget();
+
+        mLastAnimationCancelled = true;
+        mLastAnimationRunning = false;
+    }
+
+    private void clearListenerState(boolean finishAnimation) {
+        if (mRecentsAnimationListener != null) {
+            mRecentsAnimationListener.removeListener(this);
+            mRecentsAnimationListener.notifyAnimationCanceled();
+            if (mLastAnimationRunning && mLastRecentsAnimationController != null) {
+                Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(),
+                        finishAnimation
+                                ? mLastRecentsAnimationController::finishAnimationToHome
+                                : mLastRecentsAnimationController::finishAnimationToApp);
+                mLastRecentsAnimationController = null;
+                mLastAnimationTarget = null;
+            }
+        }
+        mRecentsAnimationListener = null;
+        clearAnimationTarget();
+        mLastAnimationCancelled = false;
+        mLastAnimationRunning = false;
+    }
+
+    public RecentsAnimationCallbacks newRecentsAnimationCallbacks() {
+        Preconditions.assertUIThread();
+
+        if (mLastAnimationRunning) {
+            String msg = "New animation started before completing old animation";
+            if (FeatureFlags.IS_DOGFOOD_BUILD) {
+                throw new IllegalArgumentException(msg);
+            } else {
+                Log.e("SwipeSharedState", msg, new Exception());
+            }
+        }
+
+        clearListenerState(false /* finishAnimation */);
+        boolean shouldMinimiseSplitScreen = mOverviewComponentObserver == null ? false
+                : mOverviewComponentObserver.getActivityInterface().shouldMinimizeSplitScreen();
+        mRecentsAnimationListener = new RecentsAnimationCallbacks(shouldMinimiseSplitScreen);
+        mRecentsAnimationListener.addListener(this);
+        return mRecentsAnimationListener;
+    }
+
+    public RecentsAnimationCallbacks getActiveListener() {
+        return mRecentsAnimationListener;
+    }
+
+    public void applyActiveRecentsAnimationState(RecentsAnimationListener listener) {
+        if (mLastRecentsAnimationController != null) {
+            listener.onRecentsAnimationStart(mLastRecentsAnimationController,
+                    mLastAnimationTarget);
+        } else if (mLastAnimationCancelled) {
+            listener.onRecentsAnimationCanceled(null);
+        }
+    }
+
+    /**
+     * Called when a recents animation has finished, but was interrupted before the next task was
+     * launched. The given {@param runningTaskId} should be used as the running task for the
+     * continuing input consumer.
+     */
+    public void setRecentsAnimationFinishInterrupted(int runningTaskId) {
+        recentsAnimationFinishInterrupted = true;
+        nextRunningTaskId = runningTaskId;
+        mLastAnimationTarget = mLastAnimationTarget.cloneWithoutTargets();
+    }
+
+    public void clearAllState(boolean finishAnimation) {
+        clearListenerState(finishAnimation);
+        canGestureBeContinued = false;
+        recentsAnimationFinishInterrupted = false;
+        nextRunningTaskId = -1;
+        goingToLauncher = false;
+    }
+
+    public void dump(String prefix, PrintWriter pw) {
+        pw.println(prefix + "goingToLauncher=" + goingToLauncher);
+        pw.println(prefix + "canGestureBeContinued=" + canGestureBeContinued);
+        pw.println(prefix + "recentsAnimationFinishInterrupted=" + recentsAnimationFinishInterrupted);
+        pw.println(prefix + "nextRunningTaskId=" + nextRunningTaskId);
+        pw.println(prefix + "lastAnimationCancelled=" + mLastAnimationCancelled);
+        pw.println(prefix + "lastAnimationRunning=" + mLastAnimationRunning);
+        pw.println(prefix + "logTraceId=" + mLogId);
+    }
+
+    public void setLogTraceId(int logId) {
+        this.mLogId = logId;
+    }
+}
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 b5441df..17457aa 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 TaskShortcutFactory[] MENU_OPTIONS = new TaskShortcutFactory[]{
-            TaskShortcutFactory.APP_INFO,
-            TaskShortcutFactory.SPLIT_SCREEN,
-            TaskShortcutFactory.PIN,
-            TaskShortcutFactory.INSTALL,
-            TaskShortcutFactory.FREE_FORM,
-            TaskShortcutFactory.WELLBEING
+    private static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[]{
+            new TaskSystemShortcut.AppInfo(),
+            new TaskSystemShortcut.SplitScreen(),
+            new TaskSystemShortcut.Pin(),
+            new TaskSystemShortcut.Install(),
+            new TaskSystemShortcut.Freeform()
     };
 
-    public static List<SystemShortcut> getEnabledShortcuts(TaskView taskView) {
-        final ArrayList<SystemShortcut> shortcuts = new ArrayList<>();
+    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<>();
         final BaseDraggingActivity activity = BaseActivity.fromContext(taskView.getContext());
-        for (TaskShortcutFactory menuOption : MENU_OPTIONS) {
-            SystemShortcut shortcut = menuOption.getShortcut(activity, taskView);
-            if (shortcut != null) {
-                shortcuts.add(shortcut);
+        for (TaskSystemShortcut menuOption : MENU_OPTIONS) {
+            View.OnClickListener onClickListener =
+                    menuOption.getOnClickListener(activity, taskView);
+            if (onClickListener != null) {
+                shortcuts.add(menuOption);
             }
         }
         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
deleted file mode 100644
index 9ba2e5a..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskShortcutFactory.java
+++ /dev/null
@@ -1,315 +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.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
new file mode 100644
index 0000000..5a2e3ff
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskSystemShortcut.java
@@ -0,0 +1,327 @@
+/*
+ * 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 52ae115..5591e40 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -33,8 +33,8 @@
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
 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,11 +49,12 @@
 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;
@@ -61,21 +62,20 @@
 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.quickstep.util.AssistantUtilities;
-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;
@@ -83,6 +83,7 @@
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.InputMonitorCompat;
 import com.android.systemui.shared.system.RecentsAnimationListener;
+import com.android.systemui.shared.system.TaskInfoCompat;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -111,7 +112,8 @@
  * Service connected by system-UI for handling touch interaction.
  */
 @TargetApi(Build.VERSION_CODES.Q)
-public class TouchInteractionService extends Service implements PluginListener<OverscrollPlugin> {
+public class TouchInteractionService extends Service implements
+        NavigationModeChangeListener {
 
     private static final String TAG = "TouchInteractionService";
 
@@ -120,8 +122,6 @@
     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() {
 
@@ -129,11 +129,13 @@
         public void onInitialize(Bundle bundle) {
             ISystemUiProxy proxy = ISystemUiProxy.Stub.asInterface(
                     bundle.getBinder(KEY_EXTRA_SYSUI_PROXY));
-            MAIN_EXECUTOR.execute(() -> {
-                SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(proxy);
-                TouchInteractionService.this.initInputMonitor();
-                preloadOverview(true /* fromInit */);
-            });
+            MAIN_EXECUTOR.execute(() -> SystemUiProxy.INSTANCE.get(TouchInteractionService.this)
+                    .setProxy(proxy));
+            MAIN_EXECUTOR.execute(TouchInteractionService.this::initInputMonitor);
+            MAIN_EXECUTOR.execute(() -> preloadOverview(true /* fromInit */));
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE, "TIS initialized");
+            }
             sIsInitialized = true;
         }
 
@@ -167,19 +169,15 @@
         @BinderThread
         @Override
         public void onAssistantAvailable(boolean available) {
-            MAIN_EXECUTOR.execute(() -> {
-                mDeviceState.setAssistantAvailable(available);
-                TouchInteractionService.this.onAssistantVisibilityChanged();
-            });
+            MAIN_EXECUTOR.execute(() -> mDeviceState.setAssistantAvailable(available));
+            MAIN_EXECUTOR.execute(TouchInteractionService.this::onAssistantVisibilityChanged);
         }
 
         @BinderThread
         @Override
         public void onAssistantVisibilityChanged(float visibility) {
-            MAIN_EXECUTOR.execute(() -> {
-                mDeviceState.setAssistantVisibility(visibility);
-                TouchInteractionService.this.onAssistantVisibilityChanged();
-            });
+            MAIN_EXECUTOR.execute(() -> mDeviceState.setAssistantVisibility(visibility));
+            MAIN_EXECUTOR.execute(TouchInteractionService.this::onAssistantVisibilityChanged);
         }
 
         @BinderThread
@@ -201,10 +199,8 @@
 
         @BinderThread
         public void onSystemUiStateChanged(int stateFlags) {
-            MAIN_EXECUTOR.execute(() -> {
-                mDeviceState.setSystemUiFlags(stateFlags);
-                TouchInteractionService.this.onSystemUiFlagsChanged();
-            });
+            MAIN_EXECUTOR.execute(() -> mDeviceState.setSystemUiFlags(stateFlags));
+            MAIN_EXECUTOR.execute(TouchInteractionService.this::onSystemUiFlagsChanged);
         }
 
         @BinderThread
@@ -232,6 +228,8 @@
 
     private static boolean sConnected = false;
     private static boolean sIsInitialized = false;
+    private static final SwipeSharedState sSwipeSharedState = new SwipeSharedState();
+    private int mLogId;
 
     public static boolean isConnected() {
         return sConnected;
@@ -241,38 +239,45 @@
         return sIsInitialized;
     }
 
-    private final BaseSwipeUpHandler.Factory mLauncherSwipeHandlerFactory =
-            this::createLauncherSwipeHandler;
-    private final BaseSwipeUpHandler.Factory mFallbackSwipeHandlerFactory =
-            this::createFallbackSwipeHandler;
+    public static SwipeSharedState getSwipeSharedState() {
+        return sSwipeSharedState;
+    }
+
+    private final InputConsumer mResetGestureInputConsumer =
+            new ResetGestureInputConsumer(sSwipeSharedState);
+
+    private final BaseSwipeUpHandler.Factory mWindowTreansformFactory =
+            this::createWindowTransformSwipeHandler;
+    private final BaseSwipeUpHandler.Factory mFallbackNoButtonFactory =
+            this::createFallbackNoButtonSwipeHandler;
 
     private ActivityManagerWrapper mAM;
+    private RecentsModel mRecentsModel;
     private OverviewCommandHelper mOverviewCommandHelper;
     private OverviewComponentObserver mOverviewComponentObserver;
     private InputConsumerController mInputConsumer;
     private RecentsAnimationDeviceState mDeviceState;
-    private TaskAnimationManager mTaskAnimationManager;
 
     private InputConsumer mUncheckedConsumer = InputConsumer.NO_OP;
     private InputConsumer mConsumer = InputConsumer.NO_OP;
     private Choreographer mMainChoreographer;
-    private InputConsumer mResetGestureInputConsumer;
-    private GestureState mGestureState = new GestureState();
 
     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;
     }
 
@@ -295,7 +300,7 @@
             Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "initInputMonitor 1");
         }
         disposeEventHandlers();
-        if (mDeviceState.isButtonNavMode() || !SystemUiProxy.INSTANCE.get(this).isActive()) {
+        if (!mMode.hasGestures || !SystemUiProxy.INSTANCE.get(this).isActive()) {
             return;
         }
         if (TestProtocol.sDebugTracing) {
@@ -314,22 +319,25 @@
         mDeviceState.updateGestureTouchRegions();
     }
 
-    /**
-     * Called when the navigation mode changes, guaranteed to be after the device state has updated.
-     */
-    private void onNavigationModeChanged(SysUINavigationMode.Mode mode) {
+    @Override
+    public void onNavigationModeChanged(Mode newMode) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "onNavigationModeChanged " + newMode);
+        }
+        mMode = newMode;
         initInputMonitor();
         resetHomeBounceSeenOnQuickstepEnabledFirstTime();
     }
 
     @UiThread
     public void onUserUnlocked() {
-        mTaskAnimationManager = new TaskAnimationManager();
+        mRecentsModel = RecentsModel.INSTANCE.get(this);
         mOverviewComponentObserver = new OverviewComponentObserver(this, mDeviceState);
         mOverviewCommandHelper = new OverviewCommandHelper(this, mDeviceState,
                 mOverviewComponentObserver);
-        mResetGestureInputConsumer = new ResetGestureInputConsumer(mTaskAnimationManager);
         mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer();
+
+        sSwipeSharedState.setOverviewComponentObserver(mOverviewComponentObserver);
         mInputConsumer.registerInputConsumer();
         onSystemUiFlagsChanged();
         onAssistantVisibilityChanged();
@@ -339,24 +347,10 @@
         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() {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            mOverviewComponentObserver.getActivityInterface().switchRunningTaskViewToScreenshot(
-                    null, () -> {
-                        mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */);
-                    });
-        } else {
-            mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */);
-        }
     }
 
     private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() {
-        if (!mDeviceState.isUserUnlocked() || mDeviceState.isButtonNavMode()) {
+        if (!mDeviceState.isUserUnlocked() || !mMode.hasGestures) {
             // Skip if not yet unlocked (can't read user shared prefs) or if the current navigation
             // mode doesn't have gestures
             return;
@@ -391,8 +385,9 @@
 
     @Override
     public void onDestroy() {
-        PluginManagerWrapper.INSTANCE.get(getBaseContext()).removePluginListener(this);
-
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE, "TIS destroyed");
+        }
         sIsInitialized = false;
         if (mDeviceState.isUserUnlocked()) {
             mInputConsumer.unregisterInputConsumer();
@@ -400,6 +395,7 @@
         }
         disposeEventHandlers();
         mDeviceState.destroy();
+        SysUINavigationMode.INSTANCE.get(this).removeModeChangeListener(this);
         SystemUiProxy.INSTANCE.get(this).setProxy(null);
 
         sConnected = false;
@@ -428,19 +424,19 @@
                 TraceHelper.FLAG_ALLOW_BINDER_TRACKING);
         MotionEvent event = (MotionEvent) ev;
         if (event.getAction() == ACTION_DOWN) {
-            GestureState newGestureState = new GestureState(mOverviewComponentObserver,
-                    ActiveGestureLog.INSTANCE.generateAndSetLogId());
-            newGestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.0",
-                    () -> mAM.getRunningTask(0)));
+            GestureState newGestureState = new GestureState(
+                    mOverviewComponentObserver.getActivityInterface());
+
+            mLogId = ActiveGestureLog.INSTANCE.generateAndSetLogId();
+            sSwipeSharedState.setLogTraceId(mLogId);
 
             if (mDeviceState.isInSwipeUpTouchRegion(event)) {
+                boolean useSharedState = mConsumer.useSharedSwipeState();
                 mConsumer.onConsumerAboutToBeSwitched();
-                mConsumer = newConsumer(mGestureState, newGestureState, event);
-
+                mConsumer = newConsumer(newGestureState, useSharedState, event);
                 ActiveGestureLog.INSTANCE.addLog("setInputConsumer", mConsumer.getType());
                 mUncheckedConsumer = mConsumer;
-            } else if (mDeviceState.isUserUnlocked()
-                    && mDeviceState.isFullyGesturalNavMode()
+            } else if (mDeviceState.isUserUnlocked() && mMode == Mode.NO_BUTTON
                     && 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
@@ -450,9 +446,6 @@
             } else {
                 mUncheckedConsumer = InputConsumer.NO_OP;
             }
-
-            // Save the current gesture state
-            mGestureState = newGestureState;
         }
 
         ActiveGestureLog.INSTANCE.addLog("onMotionEvent", event.getActionMasked());
@@ -460,41 +453,39 @@
         TraceHelper.INSTANCE.endFlagsOverride(traceToken);
     }
 
-    private InputConsumer newConsumer(GestureState previousGestureState,
-            GestureState newGestureState, MotionEvent event) {
+    private InputConsumer newConsumer(GestureState gestureState, boolean useSharedState,
+            MotionEvent event) {
         boolean canStartSystemGesture = mDeviceState.canStartSystemGesture();
 
         if (!mDeviceState.isUserUnlocked()) {
             if (canStartSystemGesture) {
                 // This handles apps launched in direct boot mode (e.g. dialer) as well as apps
                 // launched while device is locked even after exiting direct boot mode (e.g. camera).
-                return createDeviceLockedInputConsumer(newGestureState);
+                return createDeviceLockedInputConsumer(gestureState,
+                        mAM.getRunningTask(ACTIVITY_TYPE_ASSISTANT));
             } else {
                 return mResetGestureInputConsumer;
             }
         }
 
-        // When there is an existing recents animation running, bypass systemState check as this is
-        // a followup gesture and the first gesture started in a valid system state.
-        InputConsumer base = canStartSystemGesture
-                || previousGestureState.isRecentsAnimationRunning()
-                        ? newBaseConsumer(previousGestureState, newGestureState, event)
-                        : mResetGestureInputConsumer;
-        if (mDeviceState.isFullyGesturalNavMode()) {
+        // When using sharedState, bypass systemState check as this is a followup gesture and the
+        // first gesture started in a valid system state.
+        InputConsumer base = canStartSystemGesture || useSharedState
+                ? newBaseConsumer(gestureState, useSharedState, event) : mResetGestureInputConsumer;
+        if (mMode == Mode.NO_BUTTON) {
             if (mDeviceState.canTriggerAssistantAction(event)) {
-                base = new AssistantInputConsumer(this, newGestureState, base, mInputMonitorCompat);
+                base = new AssistantInputConsumer(this, gestureState, 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 (FeatureFlags.ENABLE_QUICK_CAPTURE_GESTURE.get()) {
+                // Put the Compose gesture as higher priority than the Assistant or base gestures
+                base = new QuickCaptureInputConsumer(this, gestureState, base, mInputMonitorCompat);
             }
 
             if (mDeviceState.isScreenPinningActive()) {
                 // Note: we only allow accessibility to wrap this, and it replaces the previous
                 // base input consumer (which should be NO_OP anyway since topTaskLocked == true).
-                base = new ScreenPinnedInputConsumer(this, newGestureState);
+                base = new ScreenPinnedInputConsumer(this, gestureState);
             }
 
             if (mDeviceState.isAccessibilityMenuAvailable()) {
@@ -509,7 +500,6 @@
         return base;
     }
 
-<<<<<<< HEAD
     private InputConsumer newBaseConsumer(GestureState gestureState, boolean useSharedState,
             MotionEvent event) {
         RunningTaskInfo runningTaskInfo = TraceHelper.whitelistIpcs("getRunningTask.0",
@@ -517,20 +507,15 @@
         if (!useSharedState) {
             sSwipeSharedState.clearAllState(false /* finishAnimation */);
         }
-=======
-    private InputConsumer newBaseConsumer(GestureState previousGestureState,
-            GestureState gestureState, MotionEvent event) {
->>>>>>> ub-launcher3-master
         if (mDeviceState.isKeyguardShowingOccluded()) {
             // This handles apps showing over the lockscreen (e.g. camera)
-            return createDeviceLockedInputConsumer(gestureState);
+            return createDeviceLockedInputConsumer(gestureState, runningTaskInfo);
         }
 
         boolean forceOverviewInputConsumer = false;
-        if (AssistantUtilities.isExcludedAssistant(gestureState.getRunningTask())) {
+        if (isExcludedAssistant(runningTaskInfo)) {
             // In the case where we are in the excluded assistant state, ignore it and treat the
             // running activity as the task behind the assistant
-<<<<<<< HEAD
 
             runningTaskInfo = TraceHelper.whitelistIpcs("getRunningTask.assistant",
                     () -> mAM.getRunningTask(ACTIVITY_TYPE_ASSISTANT));
@@ -540,90 +525,80 @@
                 forceOverviewInputConsumer =
                     runningTaskInfo.baseIntent.getComponent(). equals(homeComponent);
             }
-=======
-            gestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.assistant",
-                    () -> mAM.getRunningTask(ACTIVITY_TYPE_ASSISTANT /* ignoreActivityType */)));
-            ComponentName homeComponent = mOverviewComponentObserver.getHomeIntent().getComponent();
-            ComponentName runningComponent =
-                    gestureState.getRunningTask().baseIntent.getComponent();
-            forceOverviewInputConsumer =
-                    runningComponent != null && runningComponent.equals(homeComponent);
->>>>>>> ub-launcher3-master
         }
 
-        if (previousGestureState.getFinishingRecentsAnimationTaskId() > 0) {
+        if (runningTaskInfo == null && !sSwipeSharedState.goingToLauncher
+                && !sSwipeSharedState.recentsAnimationFinishInterrupted) {
+            return mResetGestureInputConsumer;
+        } else if (sSwipeSharedState.recentsAnimationFinishInterrupted) {
             // If the finish animation was interrupted, then continue using the other activity input
             // consumer but with the next task as the running task
             RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
-            info.id = previousGestureState.getFinishingRecentsAnimationTaskId();
-            gestureState.updateRunningTask(info);
-            return createOtherActivityInputConsumer(previousGestureState, gestureState, event);
-        } else if (gestureState.getRunningTask() == null) {
-            return mResetGestureInputConsumer;
-        } else if (previousGestureState.isRunningAnimationToLauncher()
+            info.id = sSwipeSharedState.nextRunningTaskId;
+            return createOtherActivityInputConsumer(gestureState, event, info);
+        } else if (sSwipeSharedState.goingToLauncher
                 || gestureState.getActivityInterface().isResumed()
                 || forceOverviewInputConsumer) {
-            return createOverviewInputConsumer(
-                    previousGestureState, gestureState, event, forceOverviewInputConsumer);
+            return createOverviewInputConsumer(gestureState, event);
         } else if (ENABLE_QUICKSTEP_LIVE_TILE.get()
                 && gestureState.getActivityInterface().isInLiveTileMode()) {
-            return createOverviewInputConsumer(
-                    previousGestureState, gestureState, event, forceOverviewInputConsumer);
-        } else if (mDeviceState.isGestureBlockedActivity(gestureState.getRunningTask())) {
+            return createOverviewInputConsumer(gestureState, event);
+        } else if (mDeviceState.isGestureBlockedActivity(runningTaskInfo)) {
             return mResetGestureInputConsumer;
         } else {
-            return createOtherActivityInputConsumer(previousGestureState, gestureState, event);
+            return createOtherActivityInputConsumer(gestureState, event, runningTaskInfo);
         }
     }
 
-    private InputConsumer createOtherActivityInputConsumer(GestureState previousGestureState,
-            GestureState gestureState, MotionEvent event) {
+    private boolean isExcludedAssistant(TaskInfo info) {
+        return info != null
+                && TaskInfoCompat.getActivityType(info) == ACTIVITY_TYPE_ASSISTANT
+                && (info.baseIntent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
+    }
+
+    private InputConsumer createOtherActivityInputConsumer(GestureState gestureState,
+            MotionEvent event, RunningTaskInfo runningTaskInfo) {
 
         final boolean shouldDefer;
         final BaseSwipeUpHandler.Factory factory;
 
-        if (mDeviceState.isFullyGesturalNavMode()
-                && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
-            shouldDefer = previousGestureState.getFinishingRecentsAnimationTaskId() < 0;
-            factory = mFallbackSwipeHandlerFactory;
+        if (mMode == Mode.NO_BUTTON && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
+            shouldDefer = !sSwipeSharedState.recentsAnimationFinishInterrupted;
+            factory = mFallbackNoButtonFactory;
         } else {
             shouldDefer = gestureState.getActivityInterface().deferStartingActivity(mDeviceState,
                     event);
-            factory = mLauncherSwipeHandlerFactory;
+            factory = mWindowTreansformFactory;
         }
 
         final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
-        return new OtherActivityInputConsumer(this, mDeviceState, mTaskAnimationManager,
-                gestureState, shouldDefer, this::onConsumerInactive,
-                mInputMonitorCompat, disableHorizontalSwipe, factory);
+        return new OtherActivityInputConsumer(this, mDeviceState, gestureState, runningTaskInfo,
+                shouldDefer, this::onConsumerInactive, sSwipeSharedState, mInputMonitorCompat,
+                disableHorizontalSwipe, factory, mLogId);
     }
 
-    private InputConsumer createDeviceLockedInputConsumer(GestureState gestureState) {
-        if (mDeviceState.isFullyGesturalNavMode() && gestureState.getRunningTask() != null) {
-            return new DeviceLockedInputConsumer(this, mDeviceState, mTaskAnimationManager,
-                    gestureState, mInputMonitorCompat);
+    private InputConsumer createDeviceLockedInputConsumer(GestureState gestureState,
+            RunningTaskInfo taskInfo) {
+        if (mMode == Mode.NO_BUTTON && taskInfo != null) {
+            return new DeviceLockedInputConsumer(this, mDeviceState, gestureState,
+                    sSwipeSharedState, mInputMonitorCompat, taskInfo.taskId, mLogId);
         } else {
             return mResetGestureInputConsumer;
         }
     }
 
-    public InputConsumer createOverviewInputConsumer(GestureState previousGestureState,
-            GestureState gestureState, MotionEvent event,
-            boolean forceOverviewInputConsumer) {
+    public InputConsumer createOverviewInputConsumer(GestureState gestureState, MotionEvent event) {
         BaseDraggingActivity activity = gestureState.getActivityInterface().getCreatedActivity();
         if (activity == null) {
             return mResetGestureInputConsumer;
         }
 
-        if (activity.getRootView().hasWindowFocus()
-                || previousGestureState.isRunningAnimationToLauncher()
-                || (FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
-                    && forceOverviewInputConsumer)) {
+        if (activity.getRootView().hasWindowFocus() || sSwipeSharedState.goingToLauncher) {
             return new OverviewInputConsumer(gestureState, activity, mInputMonitorCompat,
                     false /* startingInActivityBounds */);
         } else {
             final boolean disableHorizontalSwipe = mDeviceState.isInExclusionRegion(event);
-            return new OverviewWithoutFocusInputConsumer(activity, mDeviceState, gestureState,
+            return new OverviewWithoutFocusInputConsumer(activity, gestureState,
                     mInputMonitorCompat, disableHorizontalSwipe);
         }
     }
@@ -642,7 +617,7 @@
         if (!mDeviceState.isUserUnlocked()) {
             return;
         }
-        if (mDeviceState.isButtonNavMode() && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
+        if (!mMode.hasGestures && !mOverviewComponentObserver.isHomeAndOverviewSame()) {
             // Prevent the overview from being started before the real home on first boot.
             return;
         }
@@ -657,8 +632,8 @@
                 mOverviewComponentObserver.getActivityInterface();
         if (activityInterface.getCreatedActivity() == null) {
             // Make sure that UI states will be initialized.
-            activityInterface.createActivityInitListener((wasVisible) -> {
-                AppLaunchTracker.INSTANCE.get(TouchInteractionService.this);
+            activityInterface.createActivityInitListener((activity, wasVisible) -> {
+                AppLaunchTracker.INSTANCE.get(activity);
                 return false;
             }).register();
         } else if (fromInit) {
@@ -668,8 +643,9 @@
             return;
         }
 
-        mTaskAnimationManager.preloadRecentsAnimation(
-                mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState());
+        // Pass null animation handler to indicate this start is preload.
+        startRecentsActivityAsync(mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState(),
+                null);
     }
 
     @Override
@@ -709,9 +685,14 @@
             // Dump everything
             mDeviceState.dump(pw);
             pw.println("TouchState:");
+            pw.println("  navMode=" + mMode);
             boolean resumed = mOverviewComponentObserver != null
                     && mOverviewComponentObserver.getActivityInterface().isResumed();
             pw.println("  resumed=" + resumed);
+            pw.println("  useSharedState=" + mConsumer.useSharedSwipeState());
+            if (mConsumer.useSharedSwipeState()) {
+                sSwipeSharedState.dump("    ", pw);
+            }
             pw.println("  mConsumer=" + mConsumer.getName());
             pw.println("FeatureFlags:");
             pw.println("  APPLY_CONFIG_AT_RUNTIME=" + APPLY_CONFIG_AT_RUNTIME.get());
@@ -737,16 +718,20 @@
         }
     }
 
-    private BaseSwipeUpHandler createLauncherSwipeHandler(GestureState gestureState,
-            long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask) {
-        return  new LauncherSwipeHandler(this, mDeviceState, mTaskAnimationManager,
-                gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
+    private BaseSwipeUpHandler createWindowTransformSwipeHandler(GestureState gestureState,
+            RunningTaskInfo runningTask, long touchTimeMs, boolean continuingLastGesture,
+            boolean isLikelyToStartNewTask) {
+        return  new WindowTransformSwipeHandler(this, mDeviceState, gestureState, runningTask,
+                touchTimeMs, mOverviewComponentObserver, continuingLastGesture, mInputConsumer,
+                mRecentsModel);
     }
 
-    private BaseSwipeUpHandler createFallbackSwipeHandler(GestureState gestureState,
-            long touchTimeMs, boolean continuingLastGesture, boolean isLikelyToStartNewTask) {
-        return new FallbackSwipeHandler(this, mDeviceState, gestureState,
-                mInputConsumer, isLikelyToStartNewTask, continuingLastGesture);
+    private BaseSwipeUpHandler createFallbackNoButtonSwipeHandler(GestureState gestureState,
+            RunningTaskInfo runningTask, long touchTimeMs, boolean continuingLastGesture,
+            boolean isLikelyToStartNewTask) {
+        return new FallbackNoButtonInputConsumer(this, gestureState, mOverviewComponentObserver,
+                runningTask, mRecentsModel, mInputConsumer, isLikelyToStartNewTask,
+                continuingLastGesture);
     }
 
     protected boolean shouldNotifyBackGesture() {
@@ -769,14 +754,4 @@
         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/LauncherSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
similarity index 78%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
index 3d664be..1168758 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -24,15 +24,13 @@
 import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
-import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
-import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
-import static com.android.quickstep.GestureState.GestureEndTarget.NEW_TASK;
-import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
-import static com.android.quickstep.GestureState.STATE_END_TARGET_ANIMATION_FINISHED;
+import static com.android.quickstep.BaseActivityInterface.AnimationFactory.ShelfAnimState.HIDE;
+import static com.android.quickstep.BaseActivityInterface.AnimationFactory.ShelfAnimState.PEEK;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
-import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.HIDE;
-import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.PEEK;
+import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.HOME;
+import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.LAST_TASK;
+import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.NEW_TASK;
+import static com.android.quickstep.WindowTransformSwipeHandler.GestureEndTarget.RECENTS;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 
 import android.animation.Animator;
@@ -40,6 +38,7 @@
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
+import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.PointF;
@@ -69,15 +68,13 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.BaseActivityInterface.AnimationFactory;
+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;
 import com.android.quickstep.util.RectFSpringAnim;
-import com.android.quickstep.util.SharedApiCompat;
-import com.android.quickstep.util.ShelfPeekAnim;
-import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
 import com.android.quickstep.views.LiveTileOverlay;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -86,13 +83,10 @@
 import com.android.systemui.shared.system.LatencyTrackerCompat;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
-/**
- * Handles the navigation gestures when Launcher is the default home activity.
- */
 @TargetApi(Build.VERSION_CODES.O)
-public class LauncherSwipeHandler<T extends BaseDraggingActivity>
+public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
         extends BaseSwipeUpHandler<T, RecentsView> implements OnApplyWindowInsetsListener {
-    private static final String TAG = LauncherSwipeHandler.class.getSimpleName();
+    private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName();
 
     private static final String[] STATE_NAMES = DEBUG_STATES ? new String[16] : null;
 
@@ -144,6 +138,42 @@
     private static final int LAUNCHER_UI_STATES =
             STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED;
 
+    public enum GestureEndTarget {
+        HOME(1, STATE_SCALED_CONTROLLER_HOME | STATE_CAPTURE_SCREENSHOT, true, false,
+                ContainerType.WORKSPACE, false),
+
+        RECENTS(1, STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
+                | STATE_SCREENSHOT_VIEW_SHOWN, true, false, ContainerType.TASKSWITCHER, true),
+
+        NEW_TASK(0, STATE_START_NEW_TASK | STATE_CAPTURE_SCREENSHOT, false, true,
+                ContainerType.APP, true),
+
+        LAST_TASK(0, STATE_RESUME_LAST_TASK, false, true, ContainerType.APP, false);
+
+        GestureEndTarget(float endShift, int endState, boolean isLauncher, boolean canBeContinued,
+                int containerType, boolean recentsAttachedToAppWindow) {
+            this.endShift = endShift;
+            this.endState = endState;
+            this.isLauncher = isLauncher;
+            this.canBeContinued = canBeContinued;
+            this.containerType = containerType;
+            this.recentsAttachedToAppWindow = recentsAttachedToAppWindow;
+        }
+
+        /** 0 is app, 1 is overview */
+        public final float endShift;
+        /** The state to apply when we reach this final target */
+        public final int endState;
+        /** Whether the target is in the launcher activity */
+        public final boolean isLauncher;
+        /** Whether the user can start a new gesture while this one is finishing */
+        public final boolean canBeContinued;
+        /** Used to log where the user ended up after the gesture ends */
+        public final int containerType;
+        /** Whether RecentsView should be attached to the window as we animate to this target */
+        public final boolean recentsAttachedToAppWindow;
+    }
+
     public static final long MAX_SWIPE_DURATION = 350;
     public static final long MIN_SWIPE_DURATION = 80;
     public static final long MIN_OVERSHOOT_DURATION = 120;
@@ -153,6 +183,7 @@
             Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW));
     private static final String SCREENSHOT_CAPTURED_EVT = "ScreenshotCaptured";
 
+    private static final long SHELF_ANIM_DURATION = 240;
     public static final long RECENTS_ATTACH_DURATION = 300;
 
     /**
@@ -160,8 +191,10 @@
      */
     private static final int LOG_NO_OP_PAGE_INDEX = -1;
 
-    private final TaskAnimationManager mTaskAnimationManager;
+    private final RecentsAnimationDeviceState mDeviceState;
+    private final GestureState mGestureState;
 
+    private GestureEndTarget mGestureEndTarget;
     // Either RectFSpringAnim (if animating home) or ObjectAnimator (from mCurrentShift) otherwise
     private RunningWindowAnim mRunningWindowAnim;
     private boolean mIsShelfPeeking;
@@ -177,6 +210,8 @@
     private boolean mHasLauncherTransitionControllerStarted;
 
     private AnimationFactory mAnimationFactory = (t) -> { };
+    private LiveTileOverlay mLiveTileOverlay = new LiveTileOverlay();
+    private boolean mLiveTileOverlayAttached = false;
 
     private boolean mWasLauncherAlreadyVisible;
 
@@ -190,14 +225,13 @@
     private final long mTouchTimeMs;
     private long mLauncherFrameDrawnTime;
 
-    private final Runnable mOnDeferredActivityLaunch = this::onDeferredActivityLaunch;
-
-    public LauncherSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
-            TaskAnimationManager taskAnimationManager, GestureState gestureState,
-            long touchTimeMs, boolean continuingLastGesture,
-            InputConsumerController inputConsumer) {
-        super(context, deviceState, gestureState, inputConsumer);
-        mTaskAnimationManager = taskAnimationManager;
+    public WindowTransformSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
+            GestureState gestureState, RunningTaskInfo runningTaskInfo, long touchTimeMs,
+            OverviewComponentObserver overviewComponentObserver, boolean continuingLastGesture,
+            InputConsumerController inputConsumer, RecentsModel recentsModel) {
+        super(context, gestureState, overviewComponentObserver, recentsModel, inputConsumer, runningTaskInfo.id);
+        mDeviceState = deviceState;
+        mGestureState = gestureState;
         mTouchTimeMs = touchTimeMs;
         mContinuingLastGesture = continuingLastGesture;
         initStateCallbacks();
@@ -206,65 +240,62 @@
     private void initStateCallbacks() {
         mStateCallback = new MultiStateCallback(STATE_NAMES);
 
-        mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED,
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED,
                 this::onLauncherPresentAndGestureStarted);
 
-        mStateCallback.runOnceAtState(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED,
+        mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED,
                 this::initializeLauncherAnimationController);
 
-        mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN,
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN,
                 this::launcherFrameDrawn);
 
-        mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED
                         | STATE_GESTURE_CANCELLED,
                 this::resetStateForAnimationCancel);
 
-        mStateCallback.runOnceAtState(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED,
+        mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED,
                 this::sendRemoteAnimationsToAnimationFactory);
 
-        mStateCallback.runOnceAtState(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED,
+        mStateCallback.addCallback(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED,
                 this::resumeLastTask);
-        mStateCallback.runOnceAtState(STATE_START_NEW_TASK | STATE_SCREENSHOT_CAPTURED,
+        mStateCallback.addCallback(STATE_START_NEW_TASK | STATE_SCREENSHOT_CAPTURED,
                 this::startNewTask);
 
-        mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
                         | STATE_LAUNCHER_DRAWN | STATE_CAPTURE_SCREENSHOT,
                 this::switchToScreenshot);
 
-        mStateCallback.runOnceAtState(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
+        mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
                         | STATE_SCALED_CONTROLLER_RECENTS,
                 this::finishCurrentTransitionToRecents);
 
-        mStateCallback.runOnceAtState(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
+        mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED
                         | STATE_SCALED_CONTROLLER_HOME,
                 this::finishCurrentTransitionToHome);
-        mStateCallback.runOnceAtState(STATE_SCALED_CONTROLLER_HOME | STATE_CURRENT_TASK_FINISHED,
+        mStateCallback.addCallback(STATE_SCALED_CONTROLLER_HOME | STATE_CURRENT_TASK_FINISHED,
                 this::reset);
 
-        mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED
                         | STATE_LAUNCHER_DRAWN | STATE_SCALED_CONTROLLER_RECENTS
                         | STATE_CURRENT_TASK_FINISHED | STATE_GESTURE_COMPLETED
                         | STATE_GESTURE_STARTED,
                 this::setupLauncherUiAfterSwipeUpToRecentsAnimation);
 
-        mGestureState.runOnceAtState(STATE_END_TARGET_ANIMATION_FINISHED, this::onEndTargetSet);
-
-        mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
-        mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
+        mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
+        mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
                 this::invalidateHandlerWithLauncher);
-        mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED | STATE_RESUME_LAST_TASK,
+        mStateCallback.addCallback(STATE_HANDLER_INVALIDATED | STATE_RESUME_LAST_TASK,
                 this::notifyTransitionCancelled);
 
         if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            mStateCallback.addChangeListener(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT
+            mStateCallback.addChangeHandler(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT
                             | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT,
                     (b) -> mRecentsView.setRunningTaskHidden(!b));
         }
     }
 
     @Override
-    protected boolean onActivityInit(Boolean alreadyOnHome) {
-        final T activity = mActivityInterface.getCreatedActivity();
+    protected boolean onActivityInit(final T activity, Boolean alreadyOnHome) {
         if (mActivity == activity) {
             return true;
         }
@@ -290,28 +321,21 @@
 
         mStateCallback.setState(STATE_LAUNCHER_PRESENT);
         if (alreadyOnHome) {
-            onLauncherStart();
+            onLauncherStart(activity);
         } else {
-            activity.runOnceOnStart(this::onLauncherStart);
+            activity.setOnStartCallback(this::onLauncherStart);
         }
 
         setupRecentsViewUi();
-
-        if (mDeviceState.getNavMode() == TWO_BUTTONS) {
-            // If the device is in two button mode, swiping up will show overview with predictions
-            // so we need to kick off switching to the overview predictions as soon as possible
-            mActivityInterface.updateOverviewPredictionState();
-        }
         return true;
     }
 
     @Override
     protected boolean moveWindowWithRecentsScroll() {
-        return mGestureState.getEndTarget() != HOME;
+        return mGestureEndTarget != HOME;
     }
 
-    private void onLauncherStart() {
-        final T activity = mActivityInterface.getCreatedActivity();
+    private void onLauncherStart(final T activity) {
         if (mActivity != activity) {
             return;
         }
@@ -321,9 +345,9 @@
 
         // If we've already ended the gesture and are going home, don't prepare recents UI,
         // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
-        if (mGestureState.getEndTarget() != HOME) {
+        if (mGestureEndTarget != HOME) {
             Runnable initAnimFactory = () -> {
-                mAnimationFactory = mActivityInterface.prepareRecentsUI(
+                mAnimationFactory = mActivityInterface.prepareRecentsUI(mActivity,
                         mWasLauncherAlreadyVisible, true,
                         this::onAnimatorPlaybackControllerCreated);
                 maybeUpdateRecentsAttachedState(false /* animate */);
@@ -332,7 +356,7 @@
                 // Launcher is visible, but might be about to stop. Thus, if we prepare recents
                 // now, it might get overridden by moveToRestState() in onStop(). To avoid this,
                 // wait until the next gesture (and possibly launcher) starts.
-                mStateCallback.runOnceAtState(STATE_GESTURE_STARTED, initAnimFactory);
+                mStateCallback.addCallback(STATE_GESTURE_STARTED, initAnimFactory);
             } else {
                 initAnimFactory.run();
             }
@@ -376,31 +400,15 @@
         // that time by a previous window transition.
         setupRecentsViewUi();
 
-        // For the duration of the gesture, in cases where an activity is launched while the
-        // activity is not yet resumed, finish the animation to ensure we get resumed
-        mGestureState.getActivityInterface().setOnDeferredActivityLaunchCallback(
-                mOnDeferredActivityLaunch);
-
         notifyGestureStartedAsync();
     }
 
-    private void onDeferredActivityLaunch() {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            mActivityInterface.switchRunningTaskViewToScreenshot(
-                    null, () -> {
-                        mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */);
-                    });
-        } else {
-            mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */);
-        }
-    }
-
     private void setupRecentsViewUi() {
         if (mContinuingLastGesture) {
             updateSysUiFlags(mCurrentShift.value);
             return;
         }
-        mRecentsView.onGestureAnimationStart(mGestureState.getRunningTaskId());
+        mRecentsView.onGestureAnimationStart(mRunningTaskId);
     }
 
     private void launcherFrameDrawn() {
@@ -429,15 +437,16 @@
                 .getHighResLoadingState().setVisible(true);
     }
 
+    private float getTaskCurveScaleForOffsetX(float offsetX, float taskWidth) {
+        float distanceToReachEdge = mDp.widthPx / 2 + taskWidth / 2 +
+                mContext.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
+        float interpolation = Math.min(1, offsetX / distanceToReachEdge);
+        return TaskView.getCurveScaleForInterpolation(interpolation);
+    }
+
     @Override
     public void onMotionPauseChanged(boolean isPaused) {
-        setShelfState(isPaused ? PEEK : HIDE, ShelfPeekAnim.INTERPOLATOR, ShelfPeekAnim.DURATION);
-
-        if (mDeviceState.isFullyGesturalNavMode() && isPaused) {
-            // In fully gestural nav mode, switch to overview predictions once the user has paused
-            // (this is a no-op if the predictions are already in that state)
-            mActivityInterface.updateOverviewPredictionState();
-        }
+        setShelfState(isPaused ? PEEK : HIDE, OVERSHOOT_1_2, SHELF_ANIM_DURATION);
     }
 
     public void maybeUpdateRecentsAttachedState() {
@@ -452,15 +461,16 @@
      * Note this method has no effect unless the navigation mode is NO_BUTTON.
      */
     private void maybeUpdateRecentsAttachedState(boolean animate) {
-        if (!mDeviceState.isFullyGesturalNavMode() || mRecentsView == null) {
+        if (mMode != Mode.NO_BUTTON || mRecentsView == null) {
             return;
         }
-        RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets != null
-                ? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId())
-                : null;
+        RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets == null
+                ? null
+                : mRecentsAnimationTargets.findTask(mRunningTaskId);
         final boolean recentsAttachedToAppWindow;
-        if (mGestureState.getEndTarget() != null) {
-            recentsAttachedToAppWindow = mGestureState.getEndTarget().recentsAttachedToAppWindow;
+        int runningTaskIndex = mRecentsView.getRunningTaskIndex();
+        if (mGestureEndTarget != null) {
+            recentsAttachedToAppWindow = mGestureEndTarget.recentsAttachedToAppWindow;
         } else if (mContinuingLastGesture
                 && mRecentsView.getRunningTaskIndex() != mRecentsView.getNextPage()) {
             recentsAttachedToAppWindow = true;
@@ -507,10 +517,9 @@
     }
 
     private void buildAnimationController() {
-        if (mGestureState.getEndTarget() == HOME || mHasLauncherTransitionControllerStarted) {
-            // We don't want a new mLauncherTransitionController if
-            // mGestureState.getEndTarget() == HOME (it has its own animation) or if we're already
-            // animating the current controller.
+        if (mGestureEndTarget == HOME || mHasLauncherTransitionControllerStarted) {
+            // We don't want a new mLauncherTransitionController if mGestureEndTarget == HOME (it
+            // has its own animation) or if we're already animating the current controller.
             return;
         }
         initTransitionEndpoints(mActivity.getDeviceProfile());
@@ -534,7 +543,7 @@
 
     @Override
     public Intent getLaunchIntent() {
-        return mGestureState.getOverviewIntent();
+        return mOverviewComponentObserver.getOverviewIntent();
     }
 
     @Override
@@ -546,8 +555,7 @@
 
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             if (mRecentsAnimationTargets != null) {
-                LiveTileOverlay.getInstance().update(
-                        mAppWindowAnimationHelper.getCurrentRectWithInsets(),
+                mLiveTileOverlay.update(mAppWindowAnimationHelper.getCurrentRectWithInsets(),
                         mAppWindowAnimationHelper.getCurrentCornerRadius());
             }
         }
@@ -555,7 +563,7 @@
         final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
         if (passed != mPassedOverviewThreshold) {
             mPassedOverviewThreshold = passed;
-            if (!mDeviceState.isFullyGesturalNavMode()) {
+            if (mMode != Mode.NO_BUTTON) {
                 performHapticFeedback();
             }
         }
@@ -568,7 +576,7 @@
     }
 
     private void updateLauncherTransitionProgress() {
-        if (mGestureState.getEndTarget() == HOME) {
+        if (mGestureEndTarget == HOME) {
             return;
         }
         // Normalize the progress to 0 to 1, as the animation controller will clamp it to that
@@ -605,9 +613,9 @@
         super.onRecentsAnimationStart(controller, targets);
 
         // Only add the callback to enable the input consumer after we actually have the controller
-        mStateCallback.runOnceAtState(STATE_APP_CONTROLLER_RECEIVED | STATE_GESTURE_STARTED,
+        mStateCallback.addCallback(STATE_APP_CONTROLLER_RECEIVED | STATE_GESTURE_STARTED,
                 mRecentsAnimationController::enableInputConsumer);
-        mStateCallback.setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
+        setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
 
         mPassedOverviewThreshold = false;
     }
@@ -615,11 +623,9 @@
     @Override
     public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
         super.onRecentsAnimationCanceled(thumbnailData);
-        if (mRecentsView != null) {
-            mRecentsView.setRecentsAnimationTargets(null, null);
-        }
+        mRecentsView.setRecentsAnimationTargets(null, null);
         mActivityInitListener.unregister();
-        mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
+        setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
         ActiveGestureLog.INSTANCE.addLog("cancelRecentsAnimation");
     }
 
@@ -627,7 +633,7 @@
     public void onGestureStarted() {
         notifyGestureStartedAsync();
         mShiftAtGestureStart = mCurrentShift.value;
-        mStateCallback.setStateOnUiThread(STATE_GESTURE_STARTED);
+        setStateOnUiThread(STATE_GESTURE_STARTED);
         mGestureStarted = true;
     }
 
@@ -650,7 +656,7 @@
     @Override
     public void onGestureCancelled() {
         updateDisplacement(0);
-        mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
+        setStateOnUiThread(STATE_GESTURE_COMPLETED);
         mLogAction = Touch.SWIPE_NOOP;
         handleNormalGestureEnd(0, false, new PointF(), true /* isCancel */);
     }
@@ -665,7 +671,7 @@
         float flingThreshold = mContext.getResources()
                 .getDimension(R.dimen.quickstep_fling_threshold_velocity);
         boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold;
-        mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
+        setStateOnUiThread(STATE_GESTURE_COMPLETED);
 
         mLogAction = isFling ? Touch.FLING : Touch.SWIPE;
         boolean isVelocityVertical = Math.abs(velocity.y) > Math.abs(velocity.x);
@@ -680,11 +686,11 @@
 
     @Override
     protected InputConsumer createNewInputProxyHandler() {
-        endRunningWindowAnim(mGestureState.getEndTarget() == HOME /* cancel */);
+        endRunningWindowAnim(mGestureEndTarget == HOME /* cancel */);
         endLauncherTransitionController();
         if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             // Hide the task view, if not already hidden
-            setTargetAlphaProvider(LauncherSwipeHandler::getHiddenTargetAlpha);
+            setTargetAlphaProvider(WindowTransformSwipeHandler::getHiddenTargetAlpha);
         }
 
         BaseDraggingActivity activity = mActivityInterface.getCreatedActivity();
@@ -702,24 +708,6 @@
         }
     }
 
-    private void onEndTargetSet() {
-        switch (mGestureState.getEndTarget()) {
-            case HOME:
-                mStateCallback.setState(STATE_SCALED_CONTROLLER_HOME | STATE_CAPTURE_SCREENSHOT);
-                break;
-            case RECENTS:
-                mStateCallback.setState(STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT
-                        | STATE_SCREENSHOT_VIEW_SHOWN);
-                break;
-            case NEW_TASK:
-                mStateCallback.setState(STATE_START_NEW_TASK | STATE_CAPTURE_SCREENSHOT);
-                break;
-            case LAST_TASK:
-                mStateCallback.setState(STATE_RESUME_LAST_TASK);
-                break;
-        }
-    }
-
     private GestureEndTarget calculateEndTarget(PointF velocity, float endVelocity, boolean isFling,
             boolean isCancel) {
         final GestureEndTarget endTarget;
@@ -741,7 +729,7 @@
         if (!isFling) {
             if (isCancel) {
                 endTarget = LAST_TASK;
-            } else if (mDeviceState.isFullyGesturalNavMode()) {
+            } else if (mMode == Mode.NO_BUTTON) {
                 if (mIsShelfPeeking) {
                     endTarget = RECENTS;
                 } else if (goingToNewTask) {
@@ -762,9 +750,9 @@
             boolean willGoToNewTaskOnSwipeUp =
                     goingToNewTask && Math.abs(velocity.x) > Math.abs(endVelocity);
 
-            if (mDeviceState.isFullyGesturalNavMode() && isSwipeUp && !willGoToNewTaskOnSwipeUp) {
+            if (mMode == Mode.NO_BUTTON && isSwipeUp && !willGoToNewTaskOnSwipeUp) {
                 endTarget = HOME;
-            } else if (mDeviceState.isFullyGesturalNavMode() && isSwipeUp && !mIsShelfPeeking) {
+            } else if (mMode == Mode.NO_BUTTON && isSwipeUp && !mIsShelfPeeking) {
                 // If swiping at a diagonal, base end target on the faster velocity.
                 endTarget = NEW_TASK;
             } else if (isSwipeUp) {
@@ -789,7 +777,7 @@
         float currentShift = mCurrentShift.value;
         final GestureEndTarget endTarget = calculateEndTarget(velocity, endVelocity,
                 isFling, isCancel);
-        float endShift = endTarget.isLauncher ? 1 : 0;
+        float endShift = endTarget.endShift;
         final float startShift;
         Interpolator interpolator = DEACCEL;
         if (!isFling) {
@@ -804,7 +792,7 @@
             float minFlingVelocity = mContext.getResources()
                     .getDimension(R.dimen.quickstep_fling_min_velocity);
             if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) {
-                if (endTarget == RECENTS && !mDeviceState.isFullyGesturalNavMode()) {
+                if (endTarget == RECENTS && mMode != Mode.NO_BUTTON) {
                     Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams(
                             startShift, endShift, endShift, endVelocity / 1000,
                             mTransitionDragLength, mContext);
@@ -837,7 +825,7 @@
             setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
             duration = Math.max(MIN_OVERSHOOT_DURATION, duration);
         } else if (endTarget == RECENTS) {
-            LiveTileOverlay.getInstance().startIconAnimation();
+            mLiveTileOverlay.startIconAnimation();
             if (mRecentsView != null) {
                 int nearestPage = mRecentsView.getPageNearestToCenterOfScreen();
                 if (mRecentsView.getNextPage() != nearestPage) {
@@ -850,7 +838,7 @@
                 }
                 duration = Math.max(duration, mRecentsView.getScroller().getDuration());
             }
-            if (mDeviceState.isFullyGesturalNavMode()) {
+            if (mMode == Mode.NO_BUTTON) {
                 setShelfState(ShelfAnimState.OVERVIEW, interpolator, duration);
             }
         } else if (endTarget == NEW_TASK || endTarget == LAST_TASK) {
@@ -892,15 +880,14 @@
     @UiThread
     private void animateToProgressInternal(float start, float end, long duration,
             Interpolator interpolator, GestureEndTarget target, PointF velocityPxPerMs) {
-        // Set the state, but don't notify until the animation completes
-        mGestureState.setEndTarget(target, false /* isAtomic */);
+        mGestureEndTarget = target;
 
         maybeUpdateRecentsAttachedState();
 
-        if (mGestureState.getEndTarget() == HOME) {
+        if (mGestureEndTarget == HOME) {
             HomeAnimationFactory homeAnimFactory;
             if (mActivity != null) {
-                homeAnimFactory = mActivityInterface.prepareHomeUI();
+                homeAnimFactory = mActivityInterface.prepareHomeUI(mActivity);
             } else {
                 homeAnimFactory = new HomeAnimationFactory() {
                     @NonNull
@@ -917,15 +904,14 @@
                         return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
                     }
                 };
-                mStateCallback.addChangeListener(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
+                mStateCallback.addChangeHandler(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
                         isPresent -> mRecentsView.startHome());
             }
             RectFSpringAnim windowAnim = createWindowAnimationToHome(start, homeAnimFactory);
             windowAnim.addAnimatorListener(new AnimationSuccessListener() {
                 @Override
                 public void onAnimationSuccess(Animator animator) {
-                    // Finalize the state and notify of the change
-                    mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
+                    setStateOnUiThread(target.endState);
                 }
             });
             windowAnim.start(velocityPxPerMs);
@@ -950,9 +936,10 @@
                         // We are about to launch the current running task, so use LAST_TASK state
                         // instead of NEW_TASK. This could happen, for example, if our scroll is
                         // aborted after we determined the target to be NEW_TASK.
-                        mGestureState.setEndTarget(LAST_TASK);
+                        setStateOnUiThread(LAST_TASK.endState);
+                    } else {
+                        setStateOnUiThread(target.endState);
                     }
-                    mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
                 }
             });
             windowAnim.start();
@@ -960,7 +947,7 @@
         }
         // Always play the entire launcher animation when going home, since it is separate from
         // the animation that has been controlled thus far.
-        if (mGestureState.getEndTarget() == HOME) {
+        if (mGestureEndTarget == HOME) {
             start = 0;
         }
 
@@ -1012,22 +999,21 @@
                 }
                 // Make sure recents is in its final state
                 maybeUpdateRecentsAttachedState(false);
-                mActivityInterface.onSwipeUpToHomeComplete();
+                mActivityInterface.onSwipeUpToHomeComplete(mActivity);
             }
         });
         return anim;
     }
 
     @Override
-    public void onConsumerAboutToBeSwitched() {
-        if (mActivity != null) {
-            // In the off chance that the gesture ends before Launcher is started, we should clear
-            // the callback here so that it doesn't update with the wrong state
-            mActivity.clearRunOnceOnStartCallback();
-            resetLauncherListenersAndOverlays();
+    public void onConsumerAboutToBeSwitched(SwipeSharedState sharedState) {
+        if (mGestureEndTarget != null) {
+            sharedState.canGestureBeContinued = mGestureEndTarget.canBeContinued;
+            sharedState.goingToLauncher = mGestureEndTarget.isLauncher;
         }
-        if (mGestureState.getEndTarget() != null && !mGestureState.isRunningAnimationToLauncher()) {
-            cancelCurrentAnimation();
+
+        if (sharedState.canGestureBeContinued) {
+            cancelCurrentAnimation(sharedState);
         } else {
             reset();
         }
@@ -1047,15 +1033,6 @@
 
     @UiThread
     private void startNewTask() {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            mRecentsAnimationController.finish(true /* toRecents */, this::startNewTaskInternal);
-        } else {
-            startNewTaskInternal();
-        }
-    }
-
-    @UiThread
-    private void startNewTaskInternal() {
         startNewTask(STATE_HANDLER_INVALIDATED, success -> {
             if (!success) {
                 // We couldn't launch the task, so take user to overview so they can
@@ -1068,14 +1045,14 @@
     }
 
     private void reset() {
-        mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
+        setStateOnUiThread(STATE_HANDLER_INVALIDATED);
     }
 
     /**
      * Cancels any running animation so that the active target can be overriden by a new swipe
      * handle (in case of quick switch).
      */
-    private void cancelCurrentAnimation() {
+    private void cancelCurrentAnimation(SwipeSharedState sharedState) {
         mCanceled = true;
         mCurrentShift.cancelAnimation();
         if (mLauncherTransitionController != null && mLauncherTransitionController
@@ -1093,7 +1070,7 @@
                     ? newRunningTaskView.getTask().key.id
                     : -1;
             mRecentsView.setCurrentTask(newRunningTaskId);
-            mGestureState.setFinishingRecentsAnimationTaskId(newRunningTaskId);
+            sharedState.setRecentsAnimationFinishInterrupted(newRunningTaskId);
         }
     }
 
@@ -1112,7 +1089,9 @@
         endLauncherTransitionController();
 
         mRecentsView.onGestureAnimationEnd();
-        resetLauncherListenersAndOverlays();
+
+        mActivity.getRootView().setOnApplyWindowInsetsListener(null);
+        removeLiveTileOverlay();
     }
 
     private void endLauncherTransitionController() {
@@ -1123,71 +1102,58 @@
         }
     }
 
-    private void resetLauncherListenersAndOverlays() {
-        // Reset the callback for deferred activity launches
-        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            mActivityInterface.setOnDeferredActivityLaunchCallback(null);
-        }
-        mActivity.getRootView().setOnApplyWindowInsetsListener(null);
-        removeLiveTileOverlay();
-    }
-
     private void notifyTransitionCancelled() {
         mAnimationFactory.onTransitionCancelled();
     }
 
     private void resetStateForAnimationCancel() {
         boolean wasVisible = mWasLauncherAlreadyVisible || mGestureStarted;
-        mActivityInterface.onTransitionCancelled(wasVisible);
+        mActivityInterface.onTransitionCancelled(mActivity, wasVisible);
 
         // Leave the pending invisible flag, as it may be used by wallpaper open animation.
         mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER);
     }
 
     private void switchToScreenshot() {
-        final int runningTaskId = mGestureState.getRunningTaskId();
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             if (mRecentsAnimationController != null) {
-                SharedApiCompat.setWillFinishToHome(mRecentsAnimationController.getController(),
-                        true /* willFinishToHome */);
                 // Update the screenshot of the task
                 if (mTaskSnapshot == null) {
-                    mTaskSnapshot = mRecentsAnimationController.screenshotTask(runningTaskId);
+                    mTaskSnapshot = mRecentsAnimationController.screenshotTask(mRunningTaskId);
                 }
-                mRecentsView.updateThumbnail(runningTaskId, mTaskSnapshot, false /* refreshNow */);
+                mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot, false /* refreshNow */);
             }
-            mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+            setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
         } else if (!hasTargets()) {
             // If there are no targets, then we don't need to capture anything
-            mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+            setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
         } else {
             boolean finishTransitionPosted = false;
             if (mRecentsAnimationController != null) {
                 // Update the screenshot of the task
                 if (mTaskSnapshot == null) {
-                    mTaskSnapshot = mRecentsAnimationController.screenshotTask(runningTaskId);
+                    mTaskSnapshot = mRecentsAnimationController.screenshotTask(mRunningTaskId);
                 }
                 final TaskView taskView;
-                if (mGestureState.getEndTarget() == HOME) {
+                if (mGestureEndTarget == HOME) {
                     // Capture the screenshot before finishing the transition to home to ensure it's
                     // taken in the correct orientation, but no need to update the thumbnail.
                     taskView = null;
                 } else {
-                    taskView = mRecentsView.updateThumbnail(runningTaskId, mTaskSnapshot);
+                    taskView = mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot);
                 }
                 if (taskView != null && !mCanceled) {
                     // Defer finishing the animation until the next launcher frame with the
                     // new thumbnail
                     finishTransitionPosted = ViewUtils.postDraw(taskView,
-                            () -> mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED),
-                                    this::isCanceled);
+                            () -> setStateOnUiThread(STATE_SCREENSHOT_CAPTURED), this::isCanceled);
                 }
             }
             if (!finishTransitionPosted) {
                 // If we haven't posted a draw callback, set the state immediately.
                 Object traceToken = TraceHelper.INSTANCE.beginSection(SCREENSHOT_CAPTURED_EVT,
                         TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS);
-                mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
+                setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
                 TraceHelper.INSTANCE.endSection(traceToken);
             }
         }
@@ -1195,24 +1161,23 @@
 
     private void finishCurrentTransitionToRecents() {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
-        } else if (!hasTargets() || mRecentsAnimationController == null) {
-            // If there are no targets or the animation not started, then there is nothing to finish
-            mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
+            setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
+        } else if (!hasTargets()) {
+            // If there are no targets, then there is nothing to finish
+            setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
         } else {
-            mRecentsAnimationController.finish(true /* toRecents */,
-                    () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
+            synchronized (mRecentsAnimationController) {
+                mRecentsAnimationController.finish(true /* toRecents */,
+                        () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED));
+            }
         }
         ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", true);
     }
 
     private void finishCurrentTransitionToHome() {
-        if (!hasTargets() || mRecentsAnimationController == null) {
-            // If there are no targets or the animation not started, then there is nothing to finish
-            mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
-        } else {
+        synchronized (mRecentsAnimationController) {
             mRecentsAnimationController.finish(true /* toRecents */,
-                    () -> mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED),
+                    () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED),
                     true /* sendUserLeaveHint */);
         }
         ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", true);
@@ -1221,14 +1186,15 @@
 
     private void setupLauncherUiAfterSwipeUpToRecentsAnimation() {
         endLauncherTransitionController();
-        mActivityInterface.onSwipeUpToRecentsComplete();
+        mActivityInterface.onSwipeUpToRecentsComplete(mActivity);
         if (mRecentsAnimationController != null) {
             mRecentsAnimationController.setDeferCancelUntilNextTransition(true /* defer */,
                     true /* screenshot */);
         }
         mRecentsView.onSwipeUpAnimationSuccess();
 
-        SystemUiProxy.INSTANCE.get(mContext).onOverviewShown(false, TAG);
+        RecentsModel.INSTANCE.get(mContext).onOverviewShown(false, TAG);
+
         doLogGesture(RECENTS);
         reset();
     }
@@ -1238,15 +1204,20 @@
         updateFinalShift();
     }
 
-    private void addLiveTileOverlay() {
-        if (LiveTileOverlay.getInstance().attach(mActivity.getRootView().getOverlay())) {
-            mRecentsView.setLiveTileOverlayAttached(true);
+    private synchronized void addLiveTileOverlay() {
+        if (!mLiveTileOverlayAttached) {
+            mActivity.getRootView().getOverlay().add(mLiveTileOverlay);
+            mRecentsView.setLiveTileOverlay(mLiveTileOverlay);
+            mLiveTileOverlayAttached = true;
         }
     }
 
-    private void removeLiveTileOverlay() {
-        LiveTileOverlay.getInstance().detach(mActivity.getRootView().getOverlay());
-        mRecentsView.setLiveTileOverlayAttached(false);
+    private synchronized void removeLiveTileOverlay() {
+        if (mLiveTileOverlayAttached) {
+            mActivity.getRootView().getOverlay().remove(mLiveTileOverlay);
+            mRecentsView.setLiveTileOverlay(null);
+            mLiveTileOverlayAttached = false;
+        }
     }
 
     public static float getHiddenTargetAlpha(RemoteAnimationTargetCompat app, float expectedAlpha) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
index 2f73fc1..0b5129c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
@@ -23,6 +23,11 @@
     }
 
     @Override
+    public boolean useSharedSwipeState() {
+        return mDelegate.useSharedSwipeState();
+    }
+
+    @Override
     public boolean allowInterceptByParent() {
         return mDelegate.allowInterceptByParent() && mState != STATE_ACTIVE;
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index 5a34520..12b7c26 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -22,7 +22,8 @@
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.Utilities.squaredTouchSlop;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.LauncherSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
+import static com.android.quickstep.TouchInteractionService.startRecentsActivityAsync;
+import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 
 import android.content.ComponentName;
@@ -44,9 +45,9 @@
 import com.android.quickstep.MultiStateCallback;
 import com.android.quickstep.RecentsAnimationController;
 import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.SwipeSharedState;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationTargets;
-import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.util.AppWindowAnimationHelper;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.InputMonitorCompat;
@@ -75,16 +76,18 @@
 
     private final Context mContext;
     private final RecentsAnimationDeviceState mDeviceState;
-    private final TaskAnimationManager mTaskAnimationManager;
     private final GestureState mGestureState;
     private final float mTouchSlopSquared;
+    private final SwipeSharedState mSwipeSharedState;
     private final InputMonitorCompat mInputMonitorCompat;
 
     private final PointF mTouchDown = new PointF();
     private final AppWindowAnimationHelper mAppWindowAnimationHelper;
+    private int mLogId;
     private final AppWindowAnimationHelper.TransformParams mTransformParams;
     private final Point mDisplaySize;
     private final MultiStateCallback mStateCallback;
+    public final int mRunningTaskId;
 
     private VelocityTracker mVelocityTracker;
     private float mProgress;
@@ -95,23 +98,25 @@
     private RecentsAnimationTargets mRecentsAnimationTargets;
 
     public DeviceLockedInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
-            TaskAnimationManager taskAnimationManager, GestureState gestureState,
-            InputMonitorCompat inputMonitorCompat) {
+            GestureState gestureState, SwipeSharedState swipeSharedState,
+            InputMonitorCompat inputMonitorCompat, int runningTaskId, int logId) {
         mContext = context;
         mDeviceState = deviceState;
-        mTaskAnimationManager = taskAnimationManager;
         mGestureState = gestureState;
         mTouchSlopSquared = squaredTouchSlop(context);
+        mSwipeSharedState = swipeSharedState;
         mAppWindowAnimationHelper = new AppWindowAnimationHelper(context);
+        mLogId = logId;
         mTransformParams = new AppWindowAnimationHelper.TransformParams();
         mInputMonitorCompat = inputMonitorCompat;
+        mRunningTaskId = runningTaskId;
 
         // Do not use DeviceProfile as the user data might be locked
         mDisplaySize = DefaultDisplay.INSTANCE.get(context).getInfo().realSize;
 
         // Init states
         mStateCallback = new MultiStateCallback(STATE_NAMES);
-        mStateCallback.runOnceAtState(STATE_TARGET_RECEIVED | STATE_HANDLER_INVALIDATED,
+        mStateCallback.addCallback(STATE_TARGET_RECEIVED | STATE_HANDLER_INVALIDATED,
                 this::endRemoteAnimation);
 
         mVelocityTracker = VelocityTracker.obtain();
@@ -202,14 +207,16 @@
 
     private void startRecentsTransition() {
         mThresholdCrossed = true;
-        mInputMonitorCompat.pilferPointers();
-
+        RecentsAnimationCallbacks callbacks = mSwipeSharedState.newRecentsAnimationCallbacks();
+        callbacks.addListener(this);
         Intent intent = new Intent(Intent.ACTION_MAIN)
                 .addCategory(Intent.CATEGORY_DEFAULT)
                 .setComponent(new ComponentName(mContext, LockScreenRecentsActivity.class))
                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)
-                .putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
-        mTaskAnimationManager.startRecentsAnimation(mGestureState, intent, this);
+                .putExtra(INTENT_EXTRA_LOG_TRACE_ID, mLogId);
+
+        mInputMonitorCompat.pilferPointers();
+        startRecentsActivityAsync(intent, callbacks);
     }
 
     @Override
@@ -219,8 +226,7 @@
         mRecentsAnimationTargets = targets;
 
         Rect displaySize = new Rect(0, 0, mDisplaySize.x, mDisplaySize.y);
-        RemoteAnimationTargetCompat targetCompat = targets.findTask(
-                mGestureState.getRunningTaskId());
+        RemoteAnimationTargetCompat targetCompat = targets.findTask(mRunningTaskId);
         if (targetCompat != null) {
             mAppWindowAnimationHelper.updateSource(displaySize, targetCompat);
         }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
similarity index 72%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
index 24f247b..370b487 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/FallbackNoButtonInputConsumer.java
@@ -13,20 +13,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.quickstep;
+package com.android.quickstep.inputconsumers;
 
-import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
-import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
-import static com.android.quickstep.GestureState.GestureEndTarget.NEW_TASK;
-import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 import static com.android.quickstep.RecentsActivity.EXTRA_TASK_ID;
 import static com.android.quickstep.RecentsActivity.EXTRA_THUMBNAIL;
-import static com.android.quickstep.LauncherSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
+import static com.android.quickstep.WindowTransformSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
+import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.HOME;
+import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.LAST_TASK;
+import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.NEW_TASK;
+import static com.android.quickstep.inputconsumers.FallbackNoButtonInputConsumer.GestureEndTarget.RECENTS;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
+import android.app.ActivityManager.RunningTaskInfo;
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
@@ -34,25 +35,32 @@
 import android.graphics.RectF;
 import android.os.Bundle;
 
-import android.util.ArrayMap;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.util.ObjectWrapper;
 import com.android.quickstep.BaseActivityInterface.HomeAnimationFactory;
-import com.android.quickstep.GestureState.GestureEndTarget;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.BaseSwipeUpHandler;
+import com.android.quickstep.GestureState;
+import com.android.quickstep.InputConsumer;
+import com.android.quickstep.MultiStateCallback;
+import com.android.quickstep.OverviewComponentObserver;
+import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.SwipeSharedState;
 import com.android.quickstep.fallback.FallbackRecentsView;
 import com.android.quickstep.util.RectFSpringAnim;
+import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
 import com.android.systemui.shared.system.InputConsumerController;
 
-/**
- * Handles the navigation gestures when a 3rd party launcher is the default home activity.
- */
-public class FallbackSwipeHandler extends BaseSwipeUpHandler<RecentsActivity, FallbackRecentsView> {
+public class FallbackNoButtonInputConsumer extends
+        BaseSwipeUpHandler<RecentsActivity, FallbackRecentsView> {
 
     private static final String[] STATE_NAMES = DEBUG_STATES ? new String[5] : null;
 
@@ -75,41 +83,51 @@
     private static final int STATE_APP_CONTROLLER_RECEIVED =
             getFlagForIndex(4, "STATE_APP_CONTROLLER_RECEIVED");
 
-    public static class EndTargetAnimationParams {
+    public enum GestureEndTarget {
+        HOME(3, 100, 1),
+        RECENTS(1, 300, 0),
+        LAST_TASK(0, 150, 1),
+        NEW_TASK(0, 150, 1);
+
         private final float mEndProgress;
         private final long mDurationMultiplier;
         private final float mLauncherAlpha;
 
-        EndTargetAnimationParams(float endProgress, long durationMultiplier, float launcherAlpha) {
+        GestureEndTarget(float endProgress, long durationMultiplier, float launcherAlpha) {
             mEndProgress = endProgress;
             mDurationMultiplier = durationMultiplier;
             mLauncherAlpha = launcherAlpha;
         }
     }
-    private static ArrayMap<GestureEndTarget, EndTargetAnimationParams>
-            mEndTargetAnimationParams = new ArrayMap();
 
     private final AnimatedFloat mLauncherAlpha = new AnimatedFloat(this::onLauncherAlphaChanged);
 
     private boolean mIsMotionPaused = false;
+    private GestureEndTarget mEndTarget;
 
     private final boolean mInQuickSwitchMode;
     private final boolean mContinuingLastGesture;
     private final boolean mRunningOverHome;
     private final boolean mSwipeUpOverHome;
 
+    private final RunningTaskInfo mRunningTaskInfo;
+
     private final PointF mEndVelocityPxPerMs = new PointF(0, 0.5f);
     private RunningWindowAnim mFinishAnimation;
 
-    public FallbackSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
-            GestureState gestureState, InputConsumerController inputConsumer,
+    public FallbackNoButtonInputConsumer(Context context, GestureState gestureState,
+            OverviewComponentObserver overviewComponentObserver,
+            RunningTaskInfo runningTaskInfo, RecentsModel recentsModel,
+            InputConsumerController inputConsumer,
             boolean isLikelyToStartNewTask, boolean continuingLastGesture) {
-        super(context, deviceState, gestureState, inputConsumer);
+        super(context, gestureState, overviewComponentObserver, recentsModel, inputConsumer,
+                runningTaskInfo.id);
         mLauncherAlpha.value = 1;
 
+        mRunningTaskInfo = runningTaskInfo;
         mInQuickSwitchMode = isLikelyToStartNewTask || continuingLastGesture;
         mContinuingLastGesture = continuingLastGesture;
-        mRunningOverHome = ActivityManagerWrapper.isHomeTask(mGestureState.getRunningTask());
+        mRunningOverHome = ActivityManagerWrapper.isHomeTask(runningTaskInfo);
         mSwipeUpOverHome = mRunningOverHome && !mInQuickSwitchMode;
 
         if (mSwipeUpOverHome) {
@@ -118,46 +136,40 @@
             mAppWindowAnimationHelper.setBaseAlphaCallback((t, a) -> mLauncherAlpha.value);
         }
 
-        // Going home has an extra long progress to ensure that it animates into the screen
-        mEndTargetAnimationParams.put(HOME, new EndTargetAnimationParams(3, 100, 1));
-        mEndTargetAnimationParams.put(RECENTS, new EndTargetAnimationParams(1, 300, 0));
-        mEndTargetAnimationParams.put(LAST_TASK, new EndTargetAnimationParams(0, 150, 1));
-        mEndTargetAnimationParams.put(NEW_TASK, new EndTargetAnimationParams(0, 150, 1));
-
         initStateCallbacks();
     }
 
     private void initStateCallbacks() {
         mStateCallback = new MultiStateCallback(STATE_NAMES);
 
-        mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED,
+        mStateCallback.addCallback(STATE_HANDLER_INVALIDATED,
                 this::onHandlerInvalidated);
-        mStateCallback.runOnceAtState(STATE_RECENTS_PRESENT | STATE_HANDLER_INVALIDATED,
+        mStateCallback.addCallback(STATE_RECENTS_PRESENT | STATE_HANDLER_INVALIDATED,
                 this::onHandlerInvalidatedWithRecents);
 
-        mStateCallback.runOnceAtState(STATE_GESTURE_CANCELLED | STATE_APP_CONTROLLER_RECEIVED,
+        mStateCallback.addCallback(STATE_GESTURE_CANCELLED | STATE_APP_CONTROLLER_RECEIVED,
                 this::finishAnimationTargetSetAnimationComplete);
 
         if (mInQuickSwitchMode) {
-            mStateCallback.runOnceAtState(STATE_GESTURE_COMPLETED | STATE_APP_CONTROLLER_RECEIVED
+            mStateCallback.addCallback(STATE_GESTURE_COMPLETED | STATE_APP_CONTROLLER_RECEIVED
                             | STATE_RECENTS_PRESENT,
                     this::finishAnimationTargetSet);
         } else {
-            mStateCallback.runOnceAtState(STATE_GESTURE_COMPLETED | STATE_APP_CONTROLLER_RECEIVED,
+            mStateCallback.addCallback(STATE_GESTURE_COMPLETED | STATE_APP_CONTROLLER_RECEIVED,
                     this::finishAnimationTargetSet);
         }
     }
 
     private void onLauncherAlphaChanged() {
-        if (mRecentsAnimationTargets != null && mGestureState.getEndTarget() == null) {
+        if (mRecentsAnimationTargets != null && mEndTarget == null) {
             applyTransformUnchecked();
         }
     }
 
     @Override
-    protected boolean onActivityInit(Boolean alreadyOnHome) {
-        mActivity = mActivityInterface.getCreatedActivity();
-        mRecentsView = mActivity.getOverviewPanel();
+    protected boolean onActivityInit(final RecentsActivity activity, Boolean alreadyOnHome) {
+        mActivity = activity;
+        mRecentsView = activity.getOverviewPanel();
         linkRecentsViewScroll();
         mRecentsView.setDisallowScrollToClearAll(true);
         mRecentsView.getClearAllButton().setVisibilityAlpha(0);
@@ -166,12 +178,12 @@
 
         if (!mContinuingLastGesture) {
             if (mRunningOverHome) {
-                mRecentsView.onGestureAnimationStart(mGestureState.getRunningTask());
+                mRecentsView.onGestureAnimationStart(mRunningTaskInfo);
             } else {
-                mRecentsView.onGestureAnimationStart(mGestureState.getRunningTaskId());
+                mRecentsView.onGestureAnimationStart(mRunningTaskId);
             }
         }
-        mStateCallback.setStateOnUiThread(STATE_RECENTS_PRESENT);
+        setStateOnUiThread(STATE_RECENTS_PRESENT);
         return true;
     }
 
@@ -214,9 +226,9 @@
     @Override
     public Intent getLaunchIntent() {
         if (mInQuickSwitchMode || mSwipeUpOverHome) {
-            return mGestureState.getOverviewIntent();
+            return mOverviewComponentObserver.getOverviewIntent();
         } else {
-            return mGestureState.getHomeIntent();
+            return mOverviewComponentObserver.getHomeIntent();
         }
     }
 
@@ -235,8 +247,8 @@
     @Override
     public void onGestureCancelled() {
         updateDisplacement(0);
-        mGestureState.setEndTarget(LAST_TASK);
-        mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED);
+        mEndTarget = LAST_TASK;
+        setStateOnUiThread(STATE_GESTURE_CANCELLED);
     }
 
     @Override
@@ -244,29 +256,28 @@
         mEndVelocityPxPerMs.set(0, velocity.y / 1000);
         if (mInQuickSwitchMode) {
             // For now set it to non-null, it will be reset before starting the animation
-            mGestureState.setEndTarget(LAST_TASK);
+            mEndTarget = LAST_TASK;
         } else {
             float flingThreshold = mContext.getResources()
                     .getDimension(R.dimen.quickstep_fling_threshold_velocity);
             boolean isFling = Math.abs(endVelocity) > flingThreshold;
 
             if (isFling) {
-                mGestureState.setEndTarget(endVelocity < 0 ? HOME : LAST_TASK);
+                mEndTarget = endVelocity < 0 ? HOME : LAST_TASK;
             } else if (mIsMotionPaused) {
-                mGestureState.setEndTarget(RECENTS);
+                mEndTarget = RECENTS;
             } else {
-                mGestureState.setEndTarget(mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW
-                        ? HOME
-                        : LAST_TASK);
+                mEndTarget = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW ? HOME : LAST_TASK;
             }
         }
-        mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
+        setStateOnUiThread(STATE_GESTURE_COMPLETED);
     }
 
     @Override
-    public void onConsumerAboutToBeSwitched() {
-        if (mInQuickSwitchMode && mGestureState.getEndTarget() != null) {
-            mGestureState.setEndTarget(HOME);
+    public void onConsumerAboutToBeSwitched(SwipeSharedState sharedState) {
+        if (mInQuickSwitchMode && mEndTarget != null) {
+            sharedState.canGestureBeContinued = true;
+            sharedState.goingToLauncher = false;
 
             mCanceled = true;
             mCurrentShift.cancelAnimation();
@@ -282,12 +293,12 @@
                             ? newRunningTaskView.getTask().key.id
                             : -1;
                     mRecentsView.setCurrentTask(newRunningTaskId);
-                    mGestureState.setFinishingRecentsAnimationTaskId(newRunningTaskId);
+                    sharedState.setRecentsAnimationFinishInterrupted(newRunningTaskId);
                 }
                 mRecentsView.setOnScrollChangeListener(null);
             }
         } else {
-            mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
+            setStateOnUiThread(STATE_HANDLER_INVALIDATED);
         }
     }
 
@@ -308,12 +319,12 @@
     }
 
     private void finishAnimationTargetSetAnimationComplete() {
-        switch (mGestureState.getEndTarget()) {
+        switch (mEndTarget) {
             case HOME: {
                 if (mSwipeUpOverHome) {
                     mRecentsAnimationController.finish(false, null, false);
                     // Send a home intent to clear the task stack
-                    mContext.startActivity(mGestureState.getHomeIntent());
+                    mContext.startActivity(mOverviewComponentObserver.getHomeIntent());
                 } else {
                     mRecentsAnimationController.finish(true, null, true);
                 }
@@ -328,8 +339,7 @@
                     break;
                 }
 
-                final int runningTaskId = mGestureState.getRunningTaskId();
-                ThumbnailData thumbnail = mRecentsAnimationController.screenshotTask(runningTaskId);
+                ThumbnailData thumbnail = mRecentsAnimationController.screenshotTask(mRunningTaskId);
                 mRecentsAnimationController.setDeferCancelUntilNextTransition(true /* defer */,
                         false /* screenshot */);
 
@@ -338,9 +348,9 @@
 
                 Bundle extras = new Bundle();
                 extras.putBinder(EXTRA_THUMBNAIL, new ObjectWrapper<>(thumbnail));
-                extras.putInt(EXTRA_TASK_ID, runningTaskId);
+                extras.putInt(EXTRA_TASK_ID, mRunningTaskId);
 
-                Intent intent = new Intent(mGestureState.getOverviewIntent())
+                Intent intent = new Intent(mOverviewComponentObserver.getOverviewIntent())
                         .putExtras(extras);
                 mContext.startActivity(intent, options.toBundle());
                 mRecentsAnimationController.cleanupScreenshot();
@@ -352,7 +362,7 @@
             }
         }
 
-        mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
+        setStateOnUiThread(STATE_HANDLER_INVALIDATED);
     }
 
     private void finishAnimationTargetSet() {
@@ -360,20 +370,17 @@
             // Recalculate the end target, some views might have been initialized after
             // gesture has ended.
             if (mRecentsView == null || !hasTargets()) {
-                mGestureState.setEndTarget(LAST_TASK);
+                mEndTarget = LAST_TASK;
             } else {
                 final int runningTaskIndex = mRecentsView.getRunningTaskIndex();
                 final int taskToLaunch = mRecentsView.getNextPage();
-                mGestureState.setEndTarget(
-                        (runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex)
-                                ? NEW_TASK
-                                : LAST_TASK);
+                mEndTarget = (runningTaskIndex >= 0 && taskToLaunch != runningTaskIndex)
+                        ? NEW_TASK : LAST_TASK;
             }
         }
 
-        EndTargetAnimationParams params = mEndTargetAnimationParams.get(mGestureState.getEndTarget());
-        float endProgress = params.mEndProgress;
-        long duration = (long) (params.mDurationMultiplier *
+        float endProgress = mEndTarget.mEndProgress;
+        long duration = (long) (mEndTarget.mDurationMultiplier *
                 Math.abs(endProgress - mCurrentShift.value));
         if (mRecentsView != null) {
             duration = Math.max(duration, mRecentsView.getScroller().getDuration());
@@ -388,7 +395,7 @@
                 }
             };
 
-            if (mGestureState.getEndTarget() == HOME && !mRunningOverHome) {
+            if (mEndTarget == HOME && !mRunningOverHome) {
                 RectFSpringAnim anim = createWindowAnimationToHome(mCurrentShift.value, duration);
                 anim.addAnimatorListener(endListener);
                 anim.start(mEndVelocityPxPerMs);
@@ -397,7 +404,7 @@
 
                 AnimatorSet anim = new AnimatorSet();
                 anim.play(mLauncherAlpha.animateToValue(
-                        mLauncherAlpha.value, params.mLauncherAlpha));
+                        mLauncherAlpha.value, mEndTarget.mLauncherAlpha));
                 anim.play(mCurrentShift.animateToValue(mCurrentShift.value, endProgress));
 
                 anim.setDuration(duration);
@@ -422,14 +429,13 @@
         }
         applyTransformUnchecked();
 
-        mStateCallback.setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
+        setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED);
     }
 
     @Override
     public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
-        super.onRecentsAnimationCanceled(thumbnailData);
         mRecentsView.setRecentsAnimationTargets(null, null);
-        mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
+        setStateOnUiThread(STATE_HANDLER_INVALIDATED);
     }
 
     /**
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 bf2128d..02f4c40 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
@@ -26,10 +26,12 @@
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.util.TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS;
+import static com.android.quickstep.TouchInteractionService.startRecentsActivityAsync;
 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.annotation.TargetApi;
+import android.app.ActivityManager.RunningTaskInfo;
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
@@ -53,10 +55,13 @@
 import com.android.quickstep.InputConsumer;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationDeviceState;
-import com.android.quickstep.TaskAnimationManager;
+import com.android.quickstep.SwipeSharedState;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.CachedEventDispatcher;
 import com.android.quickstep.util.MotionPauseDetector;
+import com.android.quickstep.util.NavBarPosition;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
@@ -75,19 +80,21 @@
     public static final float QUICKSTEP_TOUCH_SLOP_RATIO = 3;
 
     private final RecentsAnimationDeviceState mDeviceState;
-    private final TaskAnimationManager mTaskAnimationManager;
     private final GestureState mGestureState;
-    private RecentsAnimationCallbacks mActiveCallbacks;
     private final CachedEventDispatcher mRecentsViewDispatcher = new CachedEventDispatcher();
+    private final RunningTaskInfo mRunningTask;
+    private final SwipeSharedState mSwipeSharedState;
     private final InputMonitorCompat mInputMonitorCompat;
+    private final SysUINavigationMode.Mode mMode;
     private final BaseActivityInterface mActivityInterface;
 
     private final BaseSwipeUpHandler.Factory mHandlerFactory;
 
+    private final NavBarPosition mNavBarPosition;
+
     private final Consumer<OtherActivityInputConsumer> mOnCompleteCallback;
     private final MotionPauseDetector mMotionPauseDetector;
     private final float mMotionPauseMinDisplacement;
-
     private VelocityTracker mVelocityTracker;
 
     private BaseSwipeUpHandler mInteractionHandler;
@@ -116,17 +123,20 @@
         ActivityManagerWrapper.getInstance().cancelRecentsAnimation(
                 true /* restoreHomeStackPosition */);
     };
+    private int mLogId;
 
     public OtherActivityInputConsumer(Context base, RecentsAnimationDeviceState deviceState,
-            TaskAnimationManager taskAnimationManager, GestureState gestureState,
+            GestureState gestureState, RunningTaskInfo runningTaskInfo,
             boolean isDeferredDownTarget, Consumer<OtherActivityInputConsumer> onCompleteCallback,
-            InputMonitorCompat inputMonitorCompat, boolean disableHorizontalSwipe,
-            Factory handlerFactory) {
+            SwipeSharedState swipeSharedState, InputMonitorCompat inputMonitorCompat,
+            boolean disableHorizontalSwipe, Factory handlerFactory, int logId) {
         super(base);
+        mLogId = logId;
         mDeviceState = deviceState;
-        mTaskAnimationManager = taskAnimationManager;
         mGestureState = gestureState;
         mMainThreadHandler = new Handler(Looper.getMainLooper());
+        mRunningTask = runningTaskInfo;
+        mMode = SysUINavigationMode.getMode(base);
         mHandlerFactory = handlerFactory;
         mActivityInterface = mGestureState.getActivityInterface();
 
@@ -137,8 +147,11 @@
         mVelocityTracker = VelocityTracker.obtain();
         mInputMonitorCompat = inputMonitorCompat;
 
-        boolean continuingPreviousGesture = mTaskAnimationManager.isRecentsAnimationRunning();
+        boolean continuingPreviousGesture = swipeSharedState.getActiveListener() != null;
         mIsDeferredDownTarget = !continuingPreviousGesture && isDeferredDownTarget;
+        mSwipeSharedState = swipeSharedState;
+
+        mNavBarPosition = new NavBarPosition(base);
         mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
 
         float slop = QUICKSTEP_TOUCH_SLOP_RATIO * mTouchSlop;
@@ -170,7 +183,7 @@
         if (mPassedWindowMoveSlop && mInteractionHandler != null
                 && !mRecentsViewDispatcher.hasConsumer()) {
             mRecentsViewDispatcher.setConsumer(mInteractionHandler.getRecentsViewDispatcher(
-                    mDeviceState.getNavBarPosition().getRotationMode()));
+                    mNavBarPosition.getRotationMode()));
         }
         int edgeFlags = ev.getEdgeFlags();
         ev.setEdgeFlags(edgeFlags | EDGE_NAV_BAR);
@@ -280,7 +293,7 @@
                         mInteractionHandler.updateDisplacement(displacement - mStartDisplacement);
                     }
 
-                    if (mDeviceState.isFullyGesturalNavMode()) {
+                    if (mMode == Mode.NO_BUTTON) {
                         mMotionPauseDetector.setDisallowPause(upDist < mMotionPauseMinDisplacement
                                 || isLikelyToStartNewTask);
                         mMotionPauseDetector.addPosition(displacement, ev.getEventTime());
@@ -316,22 +329,25 @@
             long touchTimeMs, boolean isLikelyToStartNewTask) {
         ActiveGestureLog.INSTANCE.addLog("startRecentsAnimation");
 
-        mInteractionHandler = mHandlerFactory.newHandler(mGestureState, touchTimeMs,
-                mTaskAnimationManager.isRecentsAnimationRunning(), isLikelyToStartNewTask);
-        mInteractionHandler.setGestureEndCallback(this::onInteractionGestureFinished);
-        mMotionPauseDetector.setOnMotionPauseListener(mInteractionHandler::onMotionPauseChanged);
-        mInteractionHandler.initWhenReady();
+        RecentsAnimationCallbacks listenerSet = mSwipeSharedState.getActiveListener();
+        final BaseSwipeUpHandler handler = mHandlerFactory.newHandler(mGestureState, mRunningTask,
+                touchTimeMs, listenerSet != null, isLikelyToStartNewTask);
 
-        if (mTaskAnimationManager.isRecentsAnimationRunning()) {
-            mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(mGestureState);
-            mActiveCallbacks.addListener(mInteractionHandler);
-            mTaskAnimationManager.notifyRecentsAnimationState(mInteractionHandler);
+        mInteractionHandler = handler;
+        handler.setGestureEndCallback(this::onInteractionGestureFinished);
+        mMotionPauseDetector.setOnMotionPauseListener(handler::onMotionPauseChanged);
+        handler.initWhenReady();
+
+        if (listenerSet != null) {
+            listenerSet.addListener(handler);
+            mSwipeSharedState.applyActiveRecentsAnimationState(handler);
             notifyGestureStarted();
         } else {
-            Intent intent = mInteractionHandler.getLaunchIntent();
-            intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
-            mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation(mGestureState, intent,
-                    mInteractionHandler);
+            RecentsAnimationCallbacks callbacks = mSwipeSharedState.newRecentsAnimationCallbacks();
+            callbacks.addListener(handler);
+            Intent intent = handler.getLaunchIntent();
+            intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, mLogId);
+            startRecentsActivityAsync(intent, callbacks);
         }
     }
 
@@ -351,10 +367,8 @@
                         ViewConfiguration.get(this).getScaledMaximumFlingVelocity());
                 float velocityX = mVelocityTracker.getXVelocity(mActivePointerId);
                 float velocityY = mVelocityTracker.getYVelocity(mActivePointerId);
-                float velocity = mDeviceState.getNavBarPosition().isRightEdge()
-                        ? velocityX
-                        : mDeviceState.getNavBarPosition().isLeftEdge()
-                                ? -velocityX
+                float velocity = mNavBarPosition.isRightEdge() ? velocityX
+                        : mNavBarPosition.isLeftEdge() ? -velocityX
                                 : velocityY;
 
                 mInteractionHandler.updateDisplacement(getDisplacement(ev) - mStartDisplacement);
@@ -388,7 +402,7 @@
             // The consumer is being switched while we are active. Set up the shared state to be
             // used by the next animation
             removeListener();
-            mInteractionHandler.onConsumerAboutToBeSwitched();
+            mInteractionHandler.onConsumerAboutToBeSwitched(mSwipeSharedState);
         }
     }
 
@@ -401,15 +415,16 @@
     }
 
     private void removeListener() {
-        if (mActiveCallbacks != null) {
-            mActiveCallbacks.removeListener(mInteractionHandler);
+        RecentsAnimationCallbacks listenerSet = mSwipeSharedState.getActiveListener();
+        if (listenerSet != null) {
+            listenerSet.removeListener(mInteractionHandler);
         }
     }
 
     private float getDisplacement(MotionEvent ev) {
-        if (mDeviceState.getNavBarPosition().isRightEdge()) {
+        if (mNavBarPosition.isRightEdge()) {
             return ev.getX() - mDownPos.x;
-        } else if (mDeviceState.getNavBarPosition().isLeftEdge()) {
+        } else if (mNavBarPosition.isLeftEdge()) {
             return mDownPos.x - ev.getX();
         } else {
             return ev.getY() - mDownPos.y;
@@ -417,6 +432,11 @@
     }
 
     @Override
+    public boolean useSharedSwipeState() {
+        return mInteractionHandler != null;
+    }
+
+    @Override
     public boolean allowInterceptByParent() {
         return !mPassedPilferInputSlop;
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
index 875ec29..50069ea 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewWithoutFocusInputConsumer.java
@@ -21,9 +21,9 @@
 import static android.view.MotionEvent.ACTION_UP;
 
 import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.content.Context;
-import android.content.Intent;
 import android.graphics.PointF;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
@@ -35,34 +35,36 @@
 import com.android.launcher3.logging.StatsLogUtils;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.quickstep.GestureState;
+import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.InputConsumer;
-import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.GestureState;
 import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.NavBarPosition;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 public class OverviewWithoutFocusInputConsumer implements InputConsumer {
 
-    private final Context mContext;
-    private final RecentsAnimationDeviceState mDeviceState;
-    private final GestureState mGestureState;
     private final InputMonitorCompat mInputMonitor;
     private final boolean mDisableHorizontalSwipe;
     private final PointF mDownPos = new PointF();
     private final float mSquaredTouchSlop;
+    private final Context mContext;
+    private final NavBarPosition mNavBarPosition;
+    private final BaseActivityInterface mActivityInterface;
 
     private boolean mInterceptedTouch;
     private VelocityTracker mVelocityTracker;
 
-    public OverviewWithoutFocusInputConsumer(Context context,
-            RecentsAnimationDeviceState deviceState, GestureState gestureState,
+    public OverviewWithoutFocusInputConsumer(Context context, GestureState gestureState,
             InputMonitorCompat inputMonitor, boolean disableHorizontalSwipe) {
-        mContext = context;
-        mDeviceState = deviceState;
-        mGestureState = gestureState;
         mInputMonitor = inputMonitor;
         mDisableHorizontalSwipe = disableHorizontalSwipe;
+        mContext = context;
+        mActivityInterface = gestureState.getActivityInterface();
         mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
+        mNavBarPosition = new NavBarPosition(context);
+
         mVelocityTracker = VelocityTracker.obtain();
     }
 
@@ -133,11 +135,8 @@
         mVelocityTracker.computeCurrentVelocity(100);
         float velocityX = mVelocityTracker.getXVelocity();
         float velocityY = mVelocityTracker.getYVelocity();
-        float velocity = mDeviceState.getNavBarPosition().isRightEdge()
-                ? -velocityX
-                : mDeviceState.getNavBarPosition().isLeftEdge()
-                        ? velocityX
-                        : -velocityY;
+        float velocity = mNavBarPosition.isRightEdge()
+                ? -velocityX : (mNavBarPosition.isLeftEdge() ? velocityX : -velocityY);
 
         final boolean triggerQuickstep;
         int touch = Touch.FLING;
@@ -151,9 +150,9 @@
         }
 
         if (triggerQuickstep) {
-            mContext.startActivity(new Intent(Intent.ACTION_MAIN)
-                    .addCategory(Intent.CATEGORY_HOME)
-                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+            mActivityInterface.closeOverlay();
+            ActivityManagerWrapper.getInstance()
+                    .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
             ActiveGestureLog.INSTANCE.addLog("startQuickstep");
             BaseActivity activity = BaseDraggingActivity.fromContext(mContext);
             int pageIndex = -1; // This number doesn't reflect workspace page index.
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/QuickCaptureInputConsumer.java
similarity index 71%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/QuickCaptureInputConsumer.java
index e3da98b..97ca730 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/QuickCaptureInputConsumer.java
@@ -24,28 +24,39 @@
 
 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 pass to an {@code OverscrollPlugin}.
- *
+ * Input consumer for handling events to launch quick capture from launcher
  * @param <T> Draggable activity subclass used by RecentsView
  */
-public class OverscrollInputConsumer<T extends BaseDraggingActivity> extends DelegateInputConsumer {
+public class QuickCaptureInputConsumer<T extends BaseDraggingActivity>
+        extends DelegateInputConsumer {
 
-    private static final String TAG = "OverscrollInputConsumer";
+    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 int ANGLE_THRESHOLD = 35; // Degrees
 
@@ -58,18 +69,14 @@
 
     private final float mSquaredSlop;
 
-    private final Context mContext;
-    private final GestureState mGestureState;
-    @Nullable private final OverscrollPlugin mPlugin;
+    private Context mContext;
 
     private RecentsView mRecentsView;
 
-    public OverscrollInputConsumer(Context context, GestureState gestureState,
-            InputConsumer delegate, InputMonitorCompat inputMonitor, OverscrollPlugin plugin) {
+    public QuickCaptureInputConsumer(Context context, GestureState gestureState,
+            InputConsumer delegate, InputMonitorCompat inputMonitor) {
         super(delegate, inputMonitor);
         mContext = context;
-        mGestureState = gestureState;
-        mPlugin = plugin;
 
         float slop = ViewConfiguration.get(context).getScaledTouchSlop();
         mSquaredSlop = slop * slop;
@@ -80,11 +87,11 @@
 
     @Override
     public int getType() {
-        return TYPE_OVERSCROLL | mDelegate.getType();
+        return TYPE_QUICK_CAPTURE | mDelegate.getType();
     }
 
-    private boolean onActivityInit(Boolean alreadyOnHome) {
-        mRecentsView = mGestureState.getActivityInterface().getCreatedActivity().getOverviewPanel();
+    private boolean onActivityInit(final BaseDraggingActivity activity, Boolean alreadyOnHome) {
+        mRecentsView = activity.getOverviewPanel();
 
         return true;
     }
@@ -140,7 +147,7 @@
                         mPassedSlop = true;
                         mStartDragPos.set(mLastPos.x, mLastPos.y);
 
-                        if (isOverscrolled()) {
+                        if (isValidQuickCaptureGesture()) {
                             setActive(ev);
                         } else {
                             mState = STATE_DELEGATE_ACTIVE;
@@ -152,8 +159,8 @@
             }
             case ACTION_CANCEL:
             case ACTION_UP:
-                if (mState != STATE_DELEGATE_ACTIVE && mPassedSlop && mPlugin != null) {
-                    mPlugin.onOverscroll(getDeviceState());
+                if (mState != STATE_DELEGATE_ACTIVE && mPassedSlop) {
+                    startQuickCapture();
                 }
 
                 mPassedSlop = false;
@@ -166,7 +173,7 @@
         }
     }
 
-    private boolean isOverscrolled() {
+    private boolean isValidQuickCaptureGesture() {
         // Make sure there isn't an app to quick switch to on our right
         boolean atRightMostApp = (mRecentsView == null || mRecentsView.getRunningTaskIndex() <= 0);
 
@@ -178,19 +185,37 @@
         return atRightMostApp && angleInBounds;
     }
 
-    private String getDeviceState() {
-        String deviceState = OverscrollPlugin.DEVICE_STATE_UNKNOWN;
+    private void startQuickCapture() {
+        // Inspect our delegate's type to figure out where the user invoked Compose
+        String deviceState = DEVICE_STATE_UNKNOWN;
         int consumerType = mDelegate.getType();
         if (((consumerType & InputConsumer.TYPE_OVERVIEW) > 0)
                 || ((consumerType & InputConsumer.TYPE_OVERVIEW_WITHOUT_FOCUS)) > 0) {
-            deviceState = OverscrollPlugin.DEVICE_STATE_LAUNCHER;
+            deviceState = DEVICE_STATE_LAUNCHER;
         } else if ((consumerType & InputConsumer.TYPE_OTHER_ACTIVITY) > 0) {
-            deviceState = OverscrollPlugin.DEVICE_STATE_APP;
+            deviceState = DEVICE_STATE_APP;
         } else if (((consumerType & InputConsumer.TYPE_RESET_GESTURE) > 0)
                 || ((consumerType & InputConsumer.TYPE_DEVICE_LOCKED) > 0)) {
-            deviceState = OverscrollPlugin.DEVICE_STATE_LOCKED;
+            deviceState = DEVICE_STATE_LOCKED;
         }
 
-        return deviceState;
+        // 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);
+        }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java
index d34b40b..e04c0c7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/ResetGestureInputConsumer.java
@@ -18,17 +18,17 @@
 import android.view.MotionEvent;
 
 import com.android.quickstep.InputConsumer;
-import com.android.quickstep.TaskAnimationManager;
+import com.android.quickstep.SwipeSharedState;
 
 /**
  * A NO_OP input consumer which also resets any pending gesture
  */
 public class ResetGestureInputConsumer implements InputConsumer {
 
-    private final TaskAnimationManager mTaskAnimationManager;
+    private final SwipeSharedState mSwipeSharedState;
 
-    public ResetGestureInputConsumer(TaskAnimationManager taskAnimationManager) {
-        mTaskAnimationManager = taskAnimationManager;
+    public ResetGestureInputConsumer(SwipeSharedState swipeSharedState) {
+        mSwipeSharedState = swipeSharedState;
     }
 
     @Override
@@ -39,8 +39,8 @@
     @Override
     public void onMotionEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN
-                && mTaskAnimationManager.isRecentsAnimationRunning()) {
-            mTaskAnimationManager.finishRunningRecentsAnimation(false /* toHome */);
+                && mSwipeSharedState.getActiveListener() != null) {
+            mSwipeSharedState.clearAllState(false /* finishAnimation */);
         }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ActiveGestureLog.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ActiveGestureLog.java
index fabfc4b..9a3bb76 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ActiveGestureLog.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ActiveGestureLog.java
@@ -33,7 +33,7 @@
      */
     public static final String INTENT_EXTRA_LOG_TRACE_ID = "INTENT_EXTRA_LOG_TRACE_ID";
 
-    private ActiveGestureLog() {
+    public ActiveGestureLog() {
         super("touch_interaction_log", 40);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
index 4a39e73..24e7f0e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
@@ -169,7 +169,7 @@
             return null;
         }
 
-        float progress = Utilities.boundToRange(params.progress, 0, 1);
+        float progress = params.progress;
         updateCurrentRect(params);
 
         SurfaceParams[] surfaceParams = new SurfaceParams[params.targetSet.unfilteredApps.length];
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AssistantUtilities.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AssistantUtilities.java
deleted file mode 100644
index 552db1f..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AssistantUtilities.java
+++ /dev/null
@@ -1,47 +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.quickstep.util;
-
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT;
-
-import android.annotation.TargetApi;
-import android.app.TaskInfo;
-import android.content.Intent;
-import android.os.Build;
-
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.TaskInfoCompat;
-
-/**
- * Utility class for interacting with the Assistant.
- */
-@TargetApi(Build.VERSION_CODES.Q)
-public final class AssistantUtilities {
-
-    /** Returns true if an Assistant activity that is excluded from recents is running. */
-    public static boolean isExcludedAssistantRunning() {
-        return isExcludedAssistant(ActivityManagerWrapper.getInstance().getRunningTask());
-    }
-
-    /** Returns true if the given task holds an Assistant activity that is excluded from recents. */
-    public static boolean isExcludedAssistant(TaskInfo info) {
-        return info != null
-            && TaskInfoCompat.getActivityType(info) == ACTIVITY_TYPE_ASSISTANT
-            && (info.baseIntent.getFlags() & Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
-    }
-
-    private AssistantUtilities() {}
-}
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
new file mode 100644
index 0000000..bbb318a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/NavBarPosition.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.launcher3.uioverrides.RecentsUiFactory.ROTATION_LANDSCAPE;
+import static com.android.launcher3.uioverrides.RecentsUiFactory.ROTATION_SEASCAPE;
+import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
+
+import android.content.Context;
+import android.view.Surface;
+
+import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.quickstep.SysUINavigationMode;
+
+/**
+ * Utility class to check nav bar position
+ */
+public class NavBarPosition {
+
+    private final SysUINavigationMode.Mode mMode;
+    private final int mDisplayRotation;
+
+    public NavBarPosition(Context context) {
+        mMode = SysUINavigationMode.getMode(context);
+        mDisplayRotation = DefaultDisplay.INSTANCE.get(context).getInfo().rotation;
+    }
+
+    public boolean isRightEdge() {
+        return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_90;
+    }
+
+    public boolean isLeftEdge() {
+        return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_270;
+    }
+
+    public RotationMode getRotationMode() {
+        return isLeftEdge() ? ROTATION_SEASCAPE
+                : (isRightEdge() ? ROTATION_LANDSCAPE : RotationMode.NORMAL);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java
deleted file mode 100644
index 41be683..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/ShelfPeekAnim.java
+++ /dev/null
@@ -1,104 +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.quickstep.util;
-
-import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_SHELF_ANIM;
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.view.animation.Interpolator;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.uioverrides.states.OverviewState;
-
-/**
- * Animates the shelf between states HIDE, PEEK, and OVERVIEW.
- */
-public class ShelfPeekAnim {
-
-    public static final Interpolator INTERPOLATOR = OVERSHOOT_1_2;
-    public static final long DURATION = 240;
-
-    private final Launcher mLauncher;
-
-    private ShelfAnimState mShelfState;
-    private boolean mIsPeeking;
-
-    public ShelfPeekAnim(Launcher launcher) {
-        mLauncher = launcher;
-    }
-
-    /**
-     * Animates to the given state, canceling the previous animation if it was still running.
-     */
-    public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator, long duration) {
-        if (mShelfState == shelfState) {
-            return;
-        }
-        mLauncher.getStateManager().cancelStateElementAnimation(INDEX_SHELF_ANIM);
-        mShelfState = shelfState;
-        mIsPeeking = mShelfState == ShelfAnimState.PEEK || mShelfState == ShelfAnimState.HIDE;
-        if (mShelfState == ShelfAnimState.CANCEL) {
-            return;
-        }
-        float shelfHiddenProgress = BACKGROUND_APP.getVerticalProgress(mLauncher);
-        float shelfOverviewProgress = OVERVIEW.getVerticalProgress(mLauncher);
-        // Peek based on default overview progress so we can see hotseat if we're showing
-        // that instead of predictions in overview.
-        float defaultOverviewProgress = OverviewState.getDefaultVerticalProgress(mLauncher);
-        float shelfPeekingProgress = shelfHiddenProgress
-                - (shelfHiddenProgress - defaultOverviewProgress) * 0.25f;
-        float toProgress = mShelfState == ShelfAnimState.HIDE
-                ? shelfHiddenProgress
-                : mShelfState == ShelfAnimState.PEEK
-                        ? shelfPeekingProgress
-                        : shelfOverviewProgress;
-        Animator shelfAnim = mLauncher.getStateManager()
-                .createStateElementAnimation(INDEX_SHELF_ANIM, toProgress);
-        shelfAnim.setInterpolator(interpolator);
-        shelfAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                mShelfState = ShelfAnimState.CANCEL;
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animator) {
-                mIsPeeking = mShelfState == ShelfAnimState.PEEK;
-            }
-        });
-        shelfAnim.setDuration(duration).start();
-    }
-
-    /** @return Whether the shelf is currently peeking or animating to or from peeking. */
-    public boolean isPeeking() {
-        return mIsPeeking;
-    }
-
-    /** The various shelf states we can animate to. */
-    public enum ShelfAnimState {
-        HIDE(true), PEEK(true), OVERVIEW(false), CANCEL(false);
-
-        ShelfAnimState(boolean shouldPreformHaptic) {
-            this.shouldPreformHaptic = shouldPreformHaptic;
-        }
-
-        public final boolean shouldPreformHaptic;
-    }
-}
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 82fbbc6..0655c73 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
@@ -102,9 +102,8 @@
     @Override
     public void startHome() {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            switchToScreenshot(null,
-                    () -> finishRecentsAnimation(true /* toRecents */,
-                            () -> mActivity.getStateManager().goToState(NORMAL)));
+            switchToScreenshot(() -> finishRecentsAnimation(true /* toRecents */,
+                    () -> mActivity.getStateManager().goToState(NORMAL)));
         } else {
             mActivity.getStateManager().goToState(NORMAL);
         }
@@ -326,8 +325,8 @@
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        PluginManagerWrapper.INSTANCE.get(getContext()).addPluginListener(
-                mRecentsExtraCardPluginListener, RecentsExtraCard.class);
+        PluginManagerWrapper.INSTANCE.get(getContext())
+                .addPluginListener(mRecentsExtraCardPluginListener, RecentsExtraCard.class);
     }
 
     @Override
@@ -378,10 +377,10 @@
     }
 
     @Override
-    public void setContentAlpha(float alpha) {
-        super.setContentAlpha(alpha);
+    public void resetTaskVisuals() {
+        super.resetTaskVisuals();
         if (mRecentsExtraViewContainer != null) {
-            mRecentsExtraViewContainer.setAlpha(alpha);
+            mRecentsExtraViewContainer.setAlpha(mContentAlpha);
         }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java
index 18eda60..a838797 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LiveTileOverlay.java
@@ -16,7 +16,6 @@
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
 import android.util.FloatProperty;
-import android.view.ViewOverlay;
 
 import com.android.launcher3.anim.Interpolators;
 
@@ -37,15 +36,6 @@
                 }
             };
 
-    private static LiveTileOverlay sInstance;
-
-    public static LiveTileOverlay getInstance() {
-        if (sInstance == null) {
-            sInstance = new LiveTileOverlay();
-        }
-        return sInstance;
-    }
-
     private final Paint mPaint = new Paint();
 
     private Rect mBoundsRect = new Rect();
@@ -56,9 +46,8 @@
 
     private boolean mDrawEnabled = true;
     private float mIconAnimationProgress = 0f;
-    private boolean mIsAttached;
 
-    private LiveTileOverlay() {
+    public LiveTileOverlay() {
         mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
     }
 
@@ -135,23 +124,6 @@
         return PixelFormat.TRANSLUCENT;
     }
 
-    public boolean attach(ViewOverlay overlay) {
-        if (overlay != null && !mIsAttached) {
-            overlay.add(this);
-            mIsAttached = true;
-            return true;
-        }
-
-        return false;
-    }
-
-    public void detach(ViewOverlay overlay) {
-        if (overlay != null) {
-            overlay.remove(this);
-            mIsAttached = false;
-        }
-    }
-
     private void setIconAnimationProgress(float progress) {
         mIconAnimationProgress = progress;
         invalidateSelf();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index eaa23a6..434a0c2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -59,7 +59,6 @@
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.Handler;
-import android.os.UserHandle;
 import android.text.Layout;
 import android.text.StaticLayout;
 import android.text.TextPaint;
@@ -105,7 +104,7 @@
 import com.android.quickstep.RecentsAnimationController;
 import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.RecentsModel;
-import com.android.quickstep.RecentsModel.TaskVisualsChangeListener;
+import com.android.quickstep.RecentsModel.TaskThumbnailChangeListener;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.ViewUtils;
@@ -127,7 +126,7 @@
 @TargetApi(Build.VERSION_CODES.P)
 public abstract class RecentsView<T extends BaseActivity> extends PagedView implements Insettable,
         TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
-        InvariantDeviceProfile.OnIDPChangeListener, TaskVisualsChangeListener {
+        InvariantDeviceProfile.OnIDPChangeListener, TaskThumbnailChangeListener {
 
     private static final String TAG = RecentsView.class.getSimpleName();
 
@@ -307,7 +306,7 @@
     private final int mEmptyMessagePadding;
     private boolean mShowEmptyMessage;
     private Layout mEmptyTextLayout;
-    private boolean mLiveTileOverlayAttached;
+    private LiveTileOverlay mLiveTileOverlay;
 
     // Keeps track of the index where the first TaskView should be
     private int mTaskViewStartIndex = 0;
@@ -383,21 +382,6 @@
         return null;
     }
 
-    @Override
-    public void onTaskIconChanged(String pkg, UserHandle user) {
-        for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView tv = getTaskViewAt(i);
-            Task task = tv.getTask();
-            if (task != null && task.key != null && pkg.equals(task.key.getPackageName())
-                    && task.key.userId == user.getIdentifier()) {
-                task.icon = null;
-                if (tv.getIconView().getDrawable() != null) {
-                    tv.onTaskListVisibilityChanged(true /* visible */);
-                }
-            }
-        }
-    }
-
     /**
      * Update the thumbnail of the task.
      * @param refreshNow Refresh immediately if it's true.
@@ -875,8 +859,8 @@
      */
     public void onSwipeUpAnimationSuccess() {
         if (getRunningTaskView() != null) {
-            float startProgress = ENABLE_QUICKSTEP_LIVE_TILE.get() && mLiveTileOverlayAttached
-                    ? LiveTileOverlay.getInstance().cancelIconAnimation()
+            float startProgress = ENABLE_QUICKSTEP_LIVE_TILE.get() && mLiveTileOverlay != null
+                    ? mLiveTileOverlay.cancelIconAnimation()
                     : 0f;
             animateUpRunningTaskIconScale(startProgress);
         }
@@ -1688,8 +1672,8 @@
 
         if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
             final int[] visibleTasks = getVisibleChildrenRange();
-            event.setFromIndex(taskViewCount - visibleTasks[1]);
-            event.setToIndex(taskViewCount - visibleTasks[0]);
+            event.setFromIndex(taskViewCount - visibleTasks[1] - 1);
+            event.setToIndex(taskViewCount - visibleTasks[0] - 1);
             event.setItemCount(taskViewCount);
         }
     }
@@ -1723,13 +1707,13 @@
         mAppWindowAnimationHelper = appWindowAnimationHelper;
     }
 
-    public void setLiveTileOverlayAttached(boolean liveTileOverlayAttached) {
-        mLiveTileOverlayAttached = liveTileOverlayAttached;
+    public void setLiveTileOverlay(LiveTileOverlay liveTileOverlay) {
+        mLiveTileOverlay = liveTileOverlay;
     }
 
     public void updateLiveTileIcon(Drawable icon) {
-        if (mLiveTileOverlayAttached) {
-            LiveTileOverlay.getInstance().setIcon(icon);
+        if (mLiveTileOverlay != null) {
+            mLiveTileOverlay.setIcon(icon);
         }
     }
 
@@ -1741,17 +1725,7 @@
             return;
         }
 
-        mRecentsAnimationController.finish(toRecents, () -> {
-            if (onFinishComplete != null) {
-                onFinishComplete.run();
-                // After we finish the recents animation, the current task id should be correctly
-                // reset so that when the task is launched from Overview later, it goes through the
-                // flow of starting a new task instead of finishing recents animation to app. A
-                // typical example of this is (1) user swipes up from app to Overview (2) user
-                // taps on QSB (3) user goes back to Overview and launch the most recent task.
-                setCurrentTask(-1);
-            }
-        });
+        mRecentsAnimationController.finish(toRecents, onFinishComplete);
     }
 
     public void setDisallowScrollToClearAll(boolean disallowScrollToClearAll) {
@@ -1854,8 +1828,8 @@
     private void updateEnabledOverlays() {
         int overlayEnabledPage = mOverlayEnabled ? getNextPage() : -1;
         int taskCount = getTaskViewCount();
-        for (int i = mTaskViewStartIndex; i < mTaskViewStartIndex + taskCount; i++) {
-            getTaskViewAtByAbsoluteIndex(i).setOverlayEnabled(i == overlayEnabledPage);
+        for (int i = 0; i < taskCount; i++) {
+            getTaskViewAt(i).setOverlayEnabled(i == overlayEnabledPage);
         }
     }
 
@@ -1876,20 +1850,20 @@
         return Math.max(insets.getSystemGestureInsets().right, insets.getSystemWindowInsetRight());
     }
 
+
     /** If it's in the live tile mode, switch the running task into screenshot mode. */
-    public void switchToScreenshot(ThumbnailData thumbnailData, Runnable onFinishRunnable) {
+    public void switchToScreenshot(Runnable onFinishRunnable) {
         TaskView taskView = getRunningTaskView();
-        if (taskView != null) {
-            taskView.setShowScreenshot(true);
-            if (thumbnailData != null) {
-                taskView.getThumbnail().setThumbnail(taskView.getTask(), thumbnailData);
-            } else {
-                taskView.getThumbnail().refresh();
+        if (taskView == null) {
+            if (onFinishRunnable != null) {
+                onFinishRunnable.run();
             }
-            ViewUtils.postDraw(taskView, onFinishRunnable);
-        } else {
-            onFinishRunnable.run();
+            return;
         }
+
+        taskView.setShowScreenshot(true);
+        taskView.getThumbnail().refresh();
+        ViewUtils.postDraw(taskView, onFinishRunnable);
     }
 
     @Override
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 80022b4..07d0796 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,6 +16,7 @@
 
 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;
@@ -25,6 +26,7 @@
 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;
@@ -39,13 +41,16 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
-import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.testing.TestProtocol;
 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.
  */
@@ -192,15 +197,22 @@
         params.topMargin = (int) -mThumbnailTopMargin;
         mTaskIcon.setLayoutParams(params);
 
-        TaskOverlayFactory.getEnabledShortcuts(taskView).forEach(this::addMenuOption);
+        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));
+        }
     }
 
-    private void addMenuOption(SystemShortcut menuOption) {
+    private void addMenuOption(TaskSystemShortcut menuOption, OnClickListener onClickListener) {
         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(menuOption);
+        menuOptionView.setOnClickListener(onClickListener);
         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 a1775f4..bfb9613 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,7 +53,6 @@
 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;
@@ -62,6 +61,7 @@
 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;
@@ -287,19 +287,11 @@
     public void launchTask(boolean animate, boolean freezeTaskList, Consumer<Boolean> resultCallback,
             Handler resultCallbackHandler) {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            RecentsView recentsView = getRecentsView();
             if (isRunningTask()) {
-                recentsView.finishRecentsAnimation(false /* toRecents */,
+                getRecentsView().finishRecentsAnimation(false /* toRecents */,
                         () -> resultCallbackHandler.post(() -> resultCallback.accept(true)));
             } else {
-                // This is a workaround against the WM issue that app open is not correctly animated
-                // when recents animation is being cleaned up (b/143774568). When that's possible,
-                // we should rely on the framework side to cancel the recents animation, and we will
-                // clean up the screenshot on the launcher side while we launch the next task.
-                recentsView.switchToScreenshot(null,
-                        () -> recentsView.finishRecentsAnimation(true /* toRecents */,
-                                () -> launchTaskInternal(animate, freezeTaskList, resultCallback,
-                                        resultCallbackHandler)));
+                launchTaskInternal(animate, freezeTaskList, resultCallback, resultCallbackHandler);
             }
         } else {
             launchTaskInternal(animate, freezeTaskList, resultCallback, resultCallbackHandler);
@@ -721,8 +713,15 @@
                         getContext().getText(R.string.accessibility_close_task)));
 
         final Context context = getContext();
-        for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this)) {
-            info.addAction(s.createAccessibilityAction(context));
+        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));
+            }
         }
 
         if (mDigitalWellBeingToast.hasLimit()) {
@@ -735,8 +734,8 @@
         final RecentsView recentsView = getRecentsView();
         final AccessibilityNodeInfo.CollectionItemInfo itemInfo =
                 AccessibilityNodeInfo.CollectionItemInfo.obtain(
-                        0, 1, recentsView.getTaskViewCount() - recentsView.indexOfChild(this) - 1,
-                        1, false);
+                        0, 1, recentsView.getChildCount() - recentsView.indexOfChild(this) - 1, 1,
+                        false);
         info.setCollectionItemInfo(itemInfo);
     }
 
@@ -753,9 +752,16 @@
             return true;
         }
 
-        for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this)) {
-            if (s.hasHandlerForAction(action)) {
-                s.onClick(this);
+        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);
+                }
                 return true;
             }
         }
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 5d9a009..98aaceb 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -33,6 +33,4 @@
     <!-- 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
deleted file mode 100644
index 9ea13c6..0000000
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ /dev/null
@@ -1,280 +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;
-
-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 com.android.quickstep.util.ShelfPeekAnim;
-
-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);
-
-    private final ShelfPeekAnim mShelfPeekAnim = new ShelfPeekAnim(this);
-
-    @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));
-    }
-
-    public ShelfPeekAnim getShelfPeekAnim() {
-        return mShelfPeekAnim;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java
index 96340b2..663b125 100644
--- a/quickstep/src/com/android/launcher3/LauncherInitListener.java
+++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java
@@ -22,7 +22,6 @@
 import android.os.CancellationSignal;
 import android.os.Handler;
 
-import com.android.launcher3.util.ActivityTracker;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.RemoteAnimationProvider;
 
@@ -33,11 +32,6 @@
 
     private RemoteAnimationProvider mRemoteAnimationProvider;
 
-    /**
-     * @param onInitListener a callback made when the activity is initialized. The callback should
-     *                       return true to continue receiving callbacks (ie. for if the activity is
-     *                       recreated).
-     */
     public LauncherInitListener(BiPredicate<Launcher, Boolean> onInitListener) {
         super(onInitListener, Launcher.ACTIVITY_TRACKER);
     }
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
deleted file mode 100644
index 5aa4388..0000000
--- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java
+++ /dev/null
@@ -1,342 +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.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) ->
-            (info.getTargetComponent() == null) ? null : 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
deleted file mode 100644
index 965b5f0..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
+++ /dev/null
@@ -1,63 +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.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 43dc882..aa0dfc3 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BackButtonAlphaHandler.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BackButtonAlphaHandler.java
@@ -16,9 +16,11 @@
 
 package com.android.launcher3.uioverrides;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 
-import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager;
 import com.android.launcher3.anim.AnimatorSetBuilder;
@@ -28,14 +30,18 @@
 
 public class BackButtonAlphaHandler implements LauncherStateManager.StateHandler {
 
-    private final BaseQuickstepLauncher mLauncher;
+    private static final String TAG = "BackButtonAlphaHandler";
 
-    public BackButtonAlphaHandler(BaseQuickstepLauncher launcher) {
+    private final Launcher mLauncher;
+
+    public BackButtonAlphaHandler(Launcher launcher) {
         mLauncher = launcher;
     }
 
     @Override
-    public void setState(LauncherState state) { }
+    public void setState(LauncherState state) {
+        UiFactory.onLauncherStateOrFocusChanged(mLauncher);
+    }
 
     @Override
     public void setStateWithAnimation(LauncherState toState,
@@ -46,8 +52,8 @@
 
         if (!SysUINavigationMode.getMode(mLauncher).hasGestures) {
             // If the nav mode is not gestural, then force back button alpha to be 1
-            UiThreadHelper.setBackButtonAlphaAsync(mLauncher,
-                    BaseQuickstepLauncher.SET_BACK_BUTTON_ALPHA, 1f, true /* animate */);
+            UiThreadHelper.setBackButtonAlphaAsync(mLauncher, UiFactory.SET_BACK_BUTTON_ALPHA, 1f,
+                    true /* animate */);
             return;
         }
 
@@ -58,8 +64,15 @@
             anim.setDuration(config.duration);
             anim.addUpdateListener(valueAnimator -> {
                 final float alpha = (float) valueAnimator.getAnimatedValue();
-                UiThreadHelper.setBackButtonAlphaAsync(mLauncher,
-                        BaseQuickstepLauncher.SET_BACK_BUTTON_ALPHA, alpha, false /* animate */);
+                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);
+                }
             });
             builder.play(anim);
         }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java b/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java
new file mode 100644
index 0000000..d8aa235
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java
@@ -0,0 +1,159 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.uioverrides;
+
+import static android.os.IBinder.FLAG_ONEWAY;
+
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
+
+/**
+ * A binder proxy transaction listener for tracking non-whitelisted binder calls.
+ */
+public class DejankBinderTracker implements Binder.ProxyTransactListener {
+    private static final String TAG = "DejankBinderTracker";
+
+    private static final Object sLock = new Object();
+    private static final HashSet<String> sWhitelistedFrameworkClasses = new HashSet<>();
+    static {
+        // Common IPCs that are ok to block the main thread.
+        sWhitelistedFrameworkClasses.add("android.view.IWindowSession");
+        sWhitelistedFrameworkClasses.add("android.os.IPowerManager");
+    }
+    private static boolean sTemporarilyIgnoreTracking = false;
+
+    // Used by the client to limit binder tracking to specific regions
+    private static boolean sTrackingAllowed = false;
+
+    private BiConsumer<String, Integer> mUnexpectedTransactionCallback;
+    private boolean mIsTracking = false;
+
+    /**
+     * Temporarily ignore blocking binder calls for the duration of this {@link Runnable}.
+     */
+    @MainThread
+    public static void whitelistIpcs(Runnable runnable) {
+        sTemporarilyIgnoreTracking = true;
+        runnable.run();
+        sTemporarilyIgnoreTracking = false;
+    }
+
+    /**
+     * Temporarily ignore blocking binder calls for the duration of this {@link Supplier}.
+     */
+    @MainThread
+    public static <T> T whitelistIpcs(Supplier<T> supplier) {
+        sTemporarilyIgnoreTracking = true;
+        T value = supplier.get();
+        sTemporarilyIgnoreTracking = false;
+        return value;
+    }
+
+    /**
+     * Enables binder tracking during a test.
+     */
+    @MainThread
+    public static void allowBinderTrackingInTests() {
+        sTrackingAllowed = true;
+    }
+
+    /**
+     * Disables binder tracking during a test.
+     */
+    @MainThread
+    public static void disallowBinderTrackingInTests() {
+        sTrackingAllowed = false;
+    }
+
+    public DejankBinderTracker(BiConsumer<String, Integer> unexpectedTransactionCallback) {
+        mUnexpectedTransactionCallback = unexpectedTransactionCallback;
+    }
+
+    @MainThread
+    public void startTracking() {
+        if (!Build.TYPE.toLowerCase(Locale.ROOT).contains("debug")
+                && !Build.TYPE.toLowerCase(Locale.ROOT).equals("eng")) {
+            Log.wtf(TAG, "Unexpected use of binder tracker in non-debug build", new Exception());
+            return;
+        }
+        if (mIsTracking) {
+            return;
+        }
+        mIsTracking = true;
+        Binder.setProxyTransactListener(this);
+    }
+
+    @MainThread
+    public void stopTracking() {
+        if (!mIsTracking) {
+            return;
+        }
+        mIsTracking = false;
+        Binder.setProxyTransactListener(null);
+    }
+
+    // Override the hidden Binder#onTransactStarted method
+    public synchronized Object onTransactStarted(IBinder binder, int transactionCode, int flags) {
+        if (!mIsTracking
+                || !sTrackingAllowed
+                || sTemporarilyIgnoreTracking
+                || (flags & FLAG_ONEWAY) == FLAG_ONEWAY
+                || !isMainThread()) {
+            return null;
+        }
+
+        String descriptor;
+        try {
+            descriptor = binder.getInterfaceDescriptor();
+            if (sWhitelistedFrameworkClasses.contains(descriptor)) {
+                return null;
+            }
+        } catch (RemoteException e) {
+            e.printStackTrace();
+            descriptor = binder.getClass().getSimpleName();
+        }
+
+        mUnexpectedTransactionCallback.accept(descriptor, transactionCode);
+        return null;
+    }
+
+    @Override
+    public Object onTransactStarted(IBinder binder, int transactionCode) {
+        // Do nothing
+        return null;
+    }
+
+    @Override
+    public void onTransactEnded(Object session) {
+        // Do nothing
+    }
+
+    public static boolean isMainThread() {
+        return Thread.currentThread() == Looper.getMainLooper().getThread();
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
new file mode 100644
index 0000000..17c681b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.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.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 3cb0088..bb72315 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.SingleAxisSwipeDetector;
+import com.android.launcher3.touch.SwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
-import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.RecentsModel;
 
 /**
  * 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, SingleAxisSwipeDetector.HORIZONTAL);
+        super(l, SwipeDetector.HORIZONTAL);
     }
 
     @Override
@@ -73,7 +73,7 @@
     protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
         super.onSwipeInteractionCompleted(targetState, logAction);
         if (mStartState == NORMAL && targetState == OVERVIEW) {
-            SystemUiProxy.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
+            RecentsModel.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 99b2a81..ef6a5e2 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -43,10 +43,11 @@
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
+import com.android.launcher3.touch.SwipeDetector;
 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;
@@ -78,7 +79,7 @@
     private boolean mFinishFastOnSecondTouch;
 
     public PortraitStatesTouchController(Launcher l, boolean allowDragToOverview) {
-        super(l, SingleAxisSwipeDetector.VERTICAL);
+        super(l, SwipeDetector.VERTICAL);
         mOverviewPortraitStateTouchHelper = new PortraitOverviewStateTouchHelper(l);
         mAllowDragToOverview = allowDragToOverview;
     }
@@ -299,7 +300,7 @@
     protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
         super.onSwipeInteractionCompleted(targetState, logAction);
         if (mStartState == NORMAL && targetState == OVERVIEW) {
-            SystemUiProxy.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
+            RecentsModel.INSTANCE.get(mLauncher).onOverviewShown(true, TAG);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index fd55e07..409bec6 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -32,12 +32,11 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.quickstep.util.ActivityInitListener;
-import com.android.quickstep.util.ShelfPeekAnim;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
+import java.util.function.BiPredicate;
 import java.util.function.Consumer;
-import java.util.function.Predicate;
 
 /**
  * Utility class which abstracts out the logical differences between Launcher and RecentsActivity.
@@ -45,26 +44,21 @@
 @TargetApi(Build.VERSION_CODES.P)
 public interface BaseActivityInterface<T extends BaseDraggingActivity> {
 
-    void onTransitionCancelled(boolean activityVisible);
+    void onTransitionCancelled(T activity, boolean activityVisible);
 
     int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect);
 
-    void onSwipeUpToRecentsComplete();
+    void onSwipeUpToRecentsComplete(T activity);
 
-    default void onSwipeUpToHomeComplete() { }
+    default void onSwipeUpToHomeComplete(T activity) { }
     void onAssistantVisibilityChanged(float visibility);
 
-    @NonNull HomeAnimationFactory prepareHomeUI();
+    @NonNull HomeAnimationFactory prepareHomeUI(T activity);
 
-    AnimationFactory prepareRecentsUI(boolean activityVisible, boolean animateActivity,
-            Consumer<AnimatorPlaybackController> callback);
+    AnimationFactory prepareRecentsUI(T activity, boolean activityVisible,
+            boolean animateActivity, Consumer<AnimatorPlaybackController> callback);
 
-    ActivityInitListener createActivityInitListener(Predicate<Boolean> onInitListener);
-
-    /**
-     * Sets a callback to be run when an activity launch happens while launcher is not yet resumed.
-     */
-    default void setOnDeferredActivityLaunchCallback(Runnable r) {}
+    ActivityInitListener createActivityInitListener(BiPredicate<T, Boolean> onInitListener);
 
     @Nullable
     T getCreatedActivity();
@@ -90,30 +84,32 @@
     }
 
     /**
-     * Updates the prediction state to the overview state.
-     */
-    default void updateOverviewPredictionState() {
-        // By default overview predictions are not supported
-    }
-
-    /**
      * Used for containerType in {@link com.android.launcher3.logging.UserEventDispatcher}
      */
     int getContainerType();
 
     boolean isInLiveTileMode();
 
-    void onLaunchTaskFailed();
+    void onLaunchTaskFailed(T activity);
 
-    void onLaunchTaskSuccess();
+    void onLaunchTaskSuccess(T activity);
 
     default void closeOverlay() { }
 
-    default void switchRunningTaskViewToScreenshot(ThumbnailData thumbnailData,
-            Runnable runnable) {}
+    default void switchToScreenshot(ThumbnailData thumbnailData, Runnable runnable) {}
 
     interface AnimationFactory {
 
+        enum ShelfAnimState {
+            HIDE(true), PEEK(true), OVERVIEW(false), CANCEL(false);
+
+            ShelfAnimState(boolean shouldPreformHaptic) {
+                this.shouldPreformHaptic = shouldPreformHaptic;
+            }
+
+            public final boolean shouldPreformHaptic;
+        }
+
         default void onRemoteAnimationReceived(RemoteAnimationTargets targets) { }
 
         void createActivityInterface(long transitionLength);
@@ -122,8 +118,8 @@
 
         default void onTransitionCancelled() { }
 
-        default void setShelfState(ShelfPeekAnim.ShelfAnimState animState,
-                Interpolator interpolator, long duration) { }
+        default void setShelfState(ShelfAnimState animState, Interpolator interpolator,
+                long duration) { }
 
         /**
          * @param attached Whether to show RecentsView alongside the app window. If false, recents
diff --git a/quickstep/src/com/android/quickstep/BaseRecentsActivity.java b/quickstep/src/com/android/quickstep/BaseRecentsActivity.java
index 5fcdc19..71833ad 100644
--- a/quickstep/src/com/android/quickstep/BaseRecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/BaseRecentsActivity.java
@@ -27,6 +27,7 @@
 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;
@@ -121,17 +122,13 @@
     @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);
+        UiFactory.onEnterAnimationComplete(this);
     }
 
     @Override
     public void onTrimMemory(int level) {
         super.onTrimMemory(level);
-        RecentsModel.INSTANCE.get(this).onTrimMemory(level);
+        UiFactory.onTrimMemory(this, level);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index ae0886b..de64227 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -15,270 +15,22 @@
  */
 package com.android.quickstep;
 
-import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-
-import android.app.ActivityManager;
-import android.content.Intent;
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import java.util.ArrayList;
 
 /**
  * Manages the state for an active system gesture, listens for events from the system and Launcher,
  * and fires events when the states change.
  */
-public class GestureState implements RecentsAnimationCallbacks.RecentsAnimationListener {
-
-    /**
-     * Defines the end targets of a gesture and the associated state.
-     */
-    public enum GestureEndTarget {
-        HOME(true, ContainerType.WORKSPACE, false),
-
-        RECENTS(true, ContainerType.TASKSWITCHER, true),
-
-        NEW_TASK(false, ContainerType.APP, true),
-
-        LAST_TASK(false, ContainerType.APP, false);
-
-        GestureEndTarget(boolean isLauncher, int containerType,
-                boolean recentsAttachedToAppWindow) {
-            this.isLauncher = isLauncher;
-            this.containerType = containerType;
-            this.recentsAttachedToAppWindow = recentsAttachedToAppWindow;
-        }
-
-        /** Whether the target is in the launcher activity. Implicitly, if the end target is going
-         to Launcher, then we can not interrupt the animation to start another gesture. */
-        public final boolean isLauncher;
-        /** Used to log where the user ended up after the gesture ends */
-        public final int containerType;
-        /** Whether RecentsView should be attached to the window as we animate to this target */
-        public final boolean recentsAttachedToAppWindow;
-    }
-
-    private static final String TAG = "GestureState";
-
-    private static final ArrayList<String> STATE_NAMES = new ArrayList<>();
-    private static int FLAG_COUNT = 0;
-    private static int getFlagForIndex(String name) {
-        if (DEBUG_STATES) {
-            STATE_NAMES.add(name);
-        }
-        int index = 1 << FLAG_COUNT;
-        FLAG_COUNT++;
-        return index;
-    }
-
-    // Called when the end target as been set
-    public static final int STATE_END_TARGET_SET =
-            getFlagForIndex("STATE_END_TARGET_SET");
-
-    // Called when the end target animation has finished
-    public static final int STATE_END_TARGET_ANIMATION_FINISHED =
-            getFlagForIndex("STATE_END_TARGET_ANIMATION_FINISHED");
-
-    // Called when the recents animation has been requested to start
-    public static final int STATE_RECENTS_ANIMATION_INITIALIZED =
-            getFlagForIndex("STATE_RECENTS_ANIMATION_INITIALIZED");
-
-    // Called when the recents animation is started and the TaskAnimationManager has been updated
-    // with the controller and targets
-    public static final int STATE_RECENTS_ANIMATION_STARTED =
-            getFlagForIndex("STATE_RECENTS_ANIMATION_STARTED");
-
-    // Called when the recents animation is canceled
-    public static final int STATE_RECENTS_ANIMATION_CANCELED =
-            getFlagForIndex("STATE_RECENTS_ANIMATION_CANCELED");
-
-    // Called when the recents animation finishes
-    public static final int STATE_RECENTS_ANIMATION_FINISHED =
-            getFlagForIndex("STATE_RECENTS_ANIMATION_FINISHED");
-
-    // Always called when the recents animation ends (regardless of cancel or finish)
-    public static final int STATE_RECENTS_ANIMATION_ENDED =
-            getFlagForIndex("STATE_RECENTS_ANIMATION_ENDED");
-
+public class GestureState {
 
     // Needed to interact with the current activity
-    private final Intent mHomeIntent;
-    private final Intent mOverviewIntent;
-    private final BaseActivityInterface mActivityInterface;
-    private final MultiStateCallback mStateCallback;
-    private final int mGestureId;
+    private BaseActivityInterface mActivityInterface;
 
-    private ActivityManager.RunningTaskInfo mRunningTask;
-    private GestureEndTarget mEndTarget;
-    // TODO: This can be removed once we stop finishing the animation when starting a new task
-    private int mFinishingRecentsAnimationTaskId = -1;
-
-    public GestureState(OverviewComponentObserver componentObserver, int gestureId) {
-        mHomeIntent = componentObserver.getHomeIntent();
-        mOverviewIntent = componentObserver.getOverviewIntent();
-        mActivityInterface = componentObserver.getActivityInterface();
-        mStateCallback = new MultiStateCallback(STATE_NAMES.toArray(new String[0]));
-        mGestureId = gestureId;
+    public GestureState(BaseActivityInterface activityInterface) {
+        mActivityInterface = activityInterface;
     }
 
-    public GestureState() {
-        // Do nothing, only used for initializing the gesture state prior to user unlock
-        mHomeIntent = new Intent();
-        mOverviewIntent = new Intent();
-        mActivityInterface = null;
-        mStateCallback = new MultiStateCallback(STATE_NAMES.toArray(new String[0]));
-        mGestureId = -1;
-    }
-
-    /**
-     * @return whether the gesture state has the provided {@param stateMask} flags set.
-     */
-    public boolean hasState(int stateMask) {
-        return mStateCallback.hasStates(stateMask);
-    }
-
-    /**
-     * Sets the given {@param stateFlag}s.
-     */
-    public void setState(int stateFlag) {
-        mStateCallback.setState(stateFlag);
-    }
-
-    /**
-     * Adds a callback for when the states matching the given {@param stateMask} is set.
-     */
-    public void runOnceAtState(int stateMask, Runnable callback) {
-        mStateCallback.runOnceAtState(stateMask, callback);
-    }
-
-    /**
-     * @return the intent for the Home component.
-     */
-    public Intent getHomeIntent() {
-        return mHomeIntent;
-    }
-
-    /**
-     * @return the intent for the Overview component.
-     */
-    public Intent getOverviewIntent() {
-        return mOverviewIntent;
-    }
-
-    /**
-     * @return the interface to the activity handing the UI updates for this gesture.
-     */
     public <T extends BaseDraggingActivity> BaseActivityInterface<T> getActivityInterface() {
         return mActivityInterface;
     }
-
-    /**
-     * @return the id for this particular gesture.
-     */
-    public int getGestureId() {
-        return mGestureId;
-    }
-
-    /**
-     * @return the running task for this gesture.
-     */
-    public ActivityManager.RunningTaskInfo getRunningTask() {
-        return mRunningTask;
-    }
-
-    /**
-     * @return the running task id for this gesture.
-     */
-    public int getRunningTaskId() {
-        return mRunningTask != null ? mRunningTask.taskId : -1;
-    }
-
-    /**
-     * Updates the running task for the gesture to be the given {@param runningTask}.
-     */
-    public void updateRunningTask(ActivityManager.RunningTaskInfo runningTask) {
-        mRunningTask = runningTask;
-    }
-
-    /**
-     * @return the end target for this gesture (if known).
-     */
-    public GestureEndTarget getEndTarget() {
-        return mEndTarget;
-    }
-
-    /**
-     * Sets the end target of this gesture and immediately notifies the state changes.
-     */
-    public void setEndTarget(GestureEndTarget target) {
-        setEndTarget(target, true /* isAtomic */);
-    }
-
-    /**
-     * Sets the end target of this gesture, but if {@param isAtomic} is {@code false}, then the
-     * caller must explicitly set {@link #STATE_END_TARGET_ANIMATION_FINISHED} themselves.
-     */
-    public void setEndTarget(GestureEndTarget target, boolean isAtomic) {
-        mEndTarget = target;
-        mStateCallback.setState(STATE_END_TARGET_SET);
-        if (isAtomic) {
-            mStateCallback.setState(STATE_END_TARGET_ANIMATION_FINISHED);
-        }
-    }
-
-    /**
-     * @return the id for the task that was about to be launched following the finish of the recents
-     * animation.  Only defined between when the finish-recents call was made and the launch
-     * activity call is made.
-     */
-    public int getFinishingRecentsAnimationTaskId() {
-        return mFinishingRecentsAnimationTaskId;
-    }
-
-    /**
-     * Sets the id for the task will be launched after the recents animation is finished. Once the
-     * animation has finished then the id will be reset to -1.
-     */
-    public void setFinishingRecentsAnimationTaskId(int taskId) {
-        mFinishingRecentsAnimationTaskId = taskId;
-        mStateCallback.runOnceAtState(STATE_RECENTS_ANIMATION_FINISHED, () -> {
-            mFinishingRecentsAnimationTaskId = -1;
-        });
-    }
-
-    /**
-     * @return whether the current gesture is still running a recents animation to a state in the
-     *         Launcher or Recents activity.
-     * Updates the running task for the gesture to be the given {@param runningTask}.
-     */
-    public boolean isRunningAnimationToLauncher() {
-        return isRecentsAnimationRunning() && mEndTarget != null && mEndTarget.isLauncher;
-    }
-
-    /**
-     * @return whether the recents animation is started but not yet ended
-     */
-    public boolean isRecentsAnimationRunning() {
-        return mStateCallback.hasStates(STATE_RECENTS_ANIMATION_INITIALIZED) &&
-                !mStateCallback.hasStates(STATE_RECENTS_ANIMATION_ENDED);
-    }
-
-    @Override
-    public void onRecentsAnimationStart(RecentsAnimationController controller,
-            RecentsAnimationTargets targets) {
-        mStateCallback.setState(STATE_RECENTS_ANIMATION_STARTED);
-    }
-
-    @Override
-    public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
-        mStateCallback.setState(STATE_RECENTS_ANIMATION_CANCELED);
-        mStateCallback.setState(STATE_RECENTS_ANIMATION_ENDED);
-    }
-
-    @Override
-    public void onRecentsAnimationFinished(RecentsAnimationController controller) {
-        mStateCallback.setState(STATE_RECENTS_ANIMATION_FINISHED);
-        mStateCallback.setState(STATE_RECENTS_ANIMATION_ENDED);
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
index 3e84e7d..62c0ded 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_OVERSCROLL = 1 << 9;
+    int TYPE_QUICK_CAPTURE = 1 << 9;
 
     String[] NAMES = new String[] {
            "TYPE_NO_OP",                    // 0
@@ -45,13 +45,17 @@
             "TYPE_SCREEN_PINNED",           // 6
             "TYPE_OVERVIEW_WITHOUT_FOCUS",  // 7
             "TYPE_RESET_GESTURE",           // 8
-            "TYPE_OVERSCROLL",              // 9
+            "TYPE_QUICK_CAPTURE",           // 9
     };
 
     InputConsumer NO_OP = () -> TYPE_NO_OP;
 
     int getType();
 
+    default boolean useSharedSwipeState() {
+        return false;
+    }
+
     /**
      * Returns true if the user has crossed the threshold for it to be an explicit action.
      */
@@ -61,8 +65,6 @@
 
     /**
      * Called by the event queue when the consumer is about to be switched to a new consumer.
-     * Consumers should update the state accordingly here before the state is passed to the new
-     * consumer.
      */
     default void onConsumerAboutToBeSwitched() { }
 
diff --git a/quickstep/src/com/android/quickstep/MultiStateCallback.java b/quickstep/src/com/android/quickstep/MultiStateCallback.java
deleted file mode 100644
index 6c65e01..0000000
--- a/quickstep/src/com/android/quickstep/MultiStateCallback.java
+++ /dev/null
@@ -1,177 +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.quickstep;
-
-import static com.android.launcher3.Utilities.postAsyncCallback;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.os.Looper;
-import android.util.Log;
-import android.util.SparseArray;
-
-import com.android.launcher3.config.FeatureFlags;
-
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.StringJoiner;
-import java.util.function.Consumer;
-
-/**
- * Utility class to help manage multiple callbacks based on different states.
- */
-public class MultiStateCallback {
-
-    private static final String TAG = "MultiStateCallback";
-    public static final boolean DEBUG_STATES = false;
-
-    private final SparseArray<LinkedList<Runnable>> mCallbacks = new SparseArray<>();
-    private final SparseArray<ArrayList<Consumer<Boolean>>> mStateChangeListeners =
-            new SparseArray<>();
-
-    private final String[] mStateNames;
-
-    private int mState = 0;
-
-    public MultiStateCallback(String[] stateNames) {
-        mStateNames = DEBUG_STATES ? stateNames : null;
-    }
-
-    /**
-     * Adds the provided state flags to the global state on the UI thread and executes any callbacks
-     * as a result.
-     */
-    public void setStateOnUiThread(int stateFlag) {
-        if (Looper.myLooper() == Looper.getMainLooper()) {
-            setState(stateFlag);
-        } else {
-            postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> setState(stateFlag));
-        }
-    }
-
-    /**
-     * Adds the provided state flags to the global state and executes any callbacks as a result.
-     */
-    public void setState(int stateFlag) {
-        if (DEBUG_STATES) {
-            Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding "
-                    + convertToFlagNames(stateFlag) + " to " + convertToFlagNames(mState));
-        }
-
-        final int oldState = mState;
-        mState = mState | stateFlag;
-
-        int count = mCallbacks.size();
-        for (int i = 0; i < count; i++) {
-            int state = mCallbacks.keyAt(i);
-
-            if ((mState & state) == state) {
-                LinkedList<Runnable> callbacks = mCallbacks.valueAt(i);
-                while (!callbacks.isEmpty()) {
-                    callbacks.pollFirst().run();
-                }
-            }
-        }
-        notifyStateChangeListeners(oldState);
-    }
-
-    /**
-     * Adds the provided state flags to the global state and executes any change handlers
-     * as a result.
-     */
-    public void clearState(int stateFlag) {
-        if (DEBUG_STATES) {
-            Log.d(TAG, "[" + System.identityHashCode(this) + "] Removing "
-                    + convertToFlagNames(stateFlag) + " from " + convertToFlagNames(mState));
-        }
-
-        int oldState = mState;
-        mState = mState & ~stateFlag;
-        notifyStateChangeListeners(oldState);
-    }
-
-    private void notifyStateChangeListeners(int oldState) {
-        int count = mStateChangeListeners.size();
-        for (int i = 0; i < count; i++) {
-            int state = mStateChangeListeners.keyAt(i);
-            boolean wasOn = (state & oldState) == state;
-            boolean isOn = (state & mState) == state;
-
-            if (wasOn != isOn) {
-                ArrayList<Consumer<Boolean>> listeners = mStateChangeListeners.valueAt(i);
-                for (Consumer<Boolean> listener : listeners) {
-                    listener.accept(isOn);
-                }
-            }
-        }
-    }
-
-    /**
-     * Sets a callback to be run when the provided states in the given {@param stateMask} is
-     * enabled. The callback is only run *once*, and if the states are already set at the time of
-     * this call then the callback will be made immediately.
-     */
-    public void runOnceAtState(int stateMask, Runnable callback) {
-        if ((mState & stateMask) == stateMask) {
-            callback.run();
-        } else {
-            final LinkedList<Runnable> callbacks;
-            if (mCallbacks.indexOfKey(stateMask) >= 0) {
-                callbacks = mCallbacks.get(stateMask);
-                if (FeatureFlags.IS_DOGFOOD_BUILD && callbacks.contains(callback)) {
-                    throw new IllegalStateException("Existing callback for state found");
-                }
-            } else {
-                callbacks = new LinkedList<>();
-                mCallbacks.put(stateMask, callbacks);
-            }
-            callbacks.add(callback);
-        }
-    }
-
-    /**
-     * Adds a persistent listener to be called states in the given {@param stateMask} are enabled
-     * or disabled.
-     */
-    public void addChangeListener(int stateMask, Consumer<Boolean> listener) {
-        final ArrayList<Consumer<Boolean>> listeners;
-        if (mStateChangeListeners.indexOfKey(stateMask) >= 0) {
-            listeners = mStateChangeListeners.get(stateMask);
-        } else {
-            listeners = new ArrayList<>();
-            mStateChangeListeners.put(stateMask, listeners);
-        }
-        listeners.add(listener);
-    }
-
-    public int getState() {
-        return mState;
-    }
-
-    public boolean hasStates(int stateMask) {
-        return (mState & stateMask) == stateMask;
-    }
-
-    private String convertToFlagNames(int flags) {
-        StringJoiner joiner = new StringJoiner(", ", "[", " (" + flags + ")]");
-        for (int i = 0; i < mStateNames.length; i++) {
-            if ((flags & (1 << i)) != 0) {
-                joiner.add(mStateNames[i]);
-            }
-        }
-        return joiner.toString();
-    }
-
-}
diff --git a/quickstep/src/com/android/quickstep/NormalizedIconLoader.java b/quickstep/src/com/android/quickstep/NormalizedIconLoader.java
new file mode 100644
index 0000000..bd6204a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/NormalizedIconLoader.java
@@ -0,0 +1,99 @@
+/*
+ * 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/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index acf61b4..2918879 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -127,7 +127,7 @@
      */
     public interface RecentsAnimationListener {
         default void onRecentsAnimationStart(RecentsAnimationController controller,
-                RecentsAnimationTargets targets) {}
+                RecentsAnimationTargets targetSet) {}
 
         /**
          * Callback from the system when the recents animation is canceled. {@param thumbnailData}
@@ -135,9 +135,6 @@
          */
         default void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {}
 
-        /**
-         * Callback made whenever the recents animation is finished.
-         */
         default void onRecentsAnimationFinished(RecentsAnimationController controller) {}
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 46af8bf..d938dc5 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -18,7 +18,6 @@
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_DOWN;
 import static android.view.MotionEvent.ACTION_UP;
-
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
@@ -49,10 +48,11 @@
     private final Consumer<RecentsAnimationController> mOnFinishedListener;
     private final boolean mShouldMinimizeSplitScreen;
 
+    private boolean mWindowThresholdCrossed = false;
+
     private InputConsumerController mInputConsumerController;
     private Supplier<InputConsumer> mInputProxySupplier;
     private InputConsumer mInputConsumer;
-    private boolean mWindowThresholdCrossed = false;
     private boolean mTouchInProgress;
     private boolean mFinishPending;
 
@@ -62,6 +62,8 @@
         mController = controller;
         mOnFinishedListener = onFinishedListener;
         mShouldMinimizeSplitScreen = shouldMinimizeSplitScreen;
+
+        setWindowThresholdCrossed(mWindowThresholdCrossed);
     }
 
     /**
@@ -69,7 +71,7 @@
      * currently being animated.
      */
     public ThumbnailData screenshotTask(int taskId) {
-        return mController.screenshotTask(taskId);
+        return mController != null ? mController.screenshotTask(taskId) : null;
     }
 
     /**
@@ -186,11 +188,6 @@
         mInputConsumerController.setInputListener(this::onInputConsumerEvent);
     }
 
-    /** @return wrapper controller. */
-    public RecentsAnimationControllerCompat getController() {
-        return mController;
-    }
-
     private void disableInputProxy() {
         if (mInputConsumer != null && mTouchInProgress) {
             long now = SystemClock.uptimeMillis();
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 81f411e..9b094f6 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -16,14 +16,10 @@
 package com.android.quickstep;
 
 import static android.content.Intent.ACTION_USER_UNLOCKED;
-
-import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED;
 import static com.android.launcher3.ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE;
 import static com.android.launcher3.ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 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;
@@ -37,37 +33,27 @@
 import android.app.ActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.res.Resources;
-import android.database.ContentObserver;
 import android.graphics.Point;
 import android.graphics.RectF;
 import android.graphics.Region;
 import android.os.Process;
-import android.provider.Settings;
 import android.text.TextUtils;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.Surface;
-
 import androidx.annotation.BinderThread;
-
 import com.android.launcher3.R;
 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.quickstep.util.NavBarPosition;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 import com.android.systemui.shared.system.SystemGestureExclusionListenerCompat;
-
 import java.io.PrintWriter;
 import java.util.ArrayList;
 
@@ -75,20 +61,17 @@
  * Manages the state of the system during a swipe up gesture.
  */
 public class RecentsAnimationDeviceState implements
-        NavigationModeChangeListener,
+        SysUINavigationMode.NavigationModeChangeListener,
         DefaultDisplay.DisplayInfoChangeListener {
 
-    private final Context mContext;
-    private final UserManagerCompat mUserManager;
-    private final SysUINavigationMode mSysUiNavMode;
-    private final DefaultDisplay mDefaultDisplay;
-    private final int mDisplayId;
-
-    private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();
+    private Context mContext;
+    private UserManagerCompat mUserManager;
+    private SysUINavigationMode mSysUiNavMode;
+    private DefaultDisplay mDefaultDisplay;
+    private int mDisplayId;
 
     private @SystemUiStateFlags int mSystemUiStateFlags;
     private SysUINavigationMode.Mode mMode = THREE_BUTTONS;
-    private NavBarPosition mNavBarPosition;
 
     private final RectF mSwipeUpTouchRegion = new RectF();
     private final Region mDeferredGestureRegion = new Region();
@@ -115,13 +98,11 @@
     private ComponentName mGestureBlockedActivity;
 
     public RecentsAnimationDeviceState(Context context) {
-        final ContentResolver resolver = context.getContentResolver();
         mContext = context;
         mUserManager = UserManagerCompat.getInstance(context);
         mSysUiNavMode = SysUINavigationMode.INSTANCE.get(context);
         mDefaultDisplay = DefaultDisplay.INSTANCE.get(context);
         mDisplayId = mDefaultDisplay.getInfo().id;
-        runOnDestroy(() -> mDefaultDisplay.removeChangeListener(this));
 
         // Register for user unlocked if necessary
         mIsUserUnlocked = mUserManager.isUserUnlocked(Process.myUserHandle());
@@ -129,7 +110,6 @@
             mContext.registerReceiver(mUserUnlockedReceiver,
                     new IntentFilter(ACTION_USER_UNLOCKED));
         }
-        runOnDestroy(() -> Utilities.unregisterReceiverSafely(mContext, mUserUnlockedReceiver));
 
         // Register for exclusion updates
         mExclusionListener = new SystemGestureExclusionListenerCompat(mDisplayId) {
@@ -140,11 +120,7 @@
                 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);
@@ -153,33 +129,18 @@
         }
     }
 
-    private void runOnDestroy(Runnable action) {
-        mOnDestroyActions.add(action);
-    }
-
     /**
      * Cleans up all the registered listeners and receivers.
      */
     public void destroy() {
-        for (Runnable r : mOnDestroyActions) {
-            r.run();
-        }
-    }
-
-    /**
-     * 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));
+        Utilities.unregisterReceiverSafely(mContext, mUserUnlockedReceiver);
+        mSysUiNavMode.removeModeChangeListener(this);
+        mDefaultDisplay.removeChangeListener(this);
+        mExclusionListener.unregister();
     }
 
     @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);
@@ -191,7 +152,6 @@
             mExclusionListener.unregister();
         }
         mMode = newMode;
-        mNavBarPosition = new NavBarPosition(mMode, mDefaultDisplay.getInfo());
     }
 
     @Override
@@ -200,46 +160,10 @@
             return;
         }
 
-        mNavBarPosition = new NavBarPosition(mMode, info);
         updateGestureTouchRegions();
     }
 
     /**
-     * @return the current navigation mode for the device.
-     */
-    public SysUINavigationMode.Mode getNavMode() {
-        return mMode;
-    }
-
-    /**
-     * @return the nav bar position for the current nav bar mode and display rotation.
-     */
-    public NavBarPosition getNavBarPosition() {
-        return mNavBarPosition;
-    }
-
-    /**
-     * @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() {
@@ -277,7 +201,7 @@
      * @return whether the given running task info matches the gesture-blocked activity.
      */
     public boolean isGestureBlockedActivity(ActivityManager.RunningTaskInfo runningTaskInfo) {
-        return runningTaskInfo != null && mGestureBlockedActivity != null
+        return runningTaskInfo != null
                 && mGestureBlockedActivity.equals(runningTaskInfo.topActivity);
     }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
index 718c5ba..9353759 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
@@ -41,4 +41,13 @@
     public boolean hasTargets() {
         return unfilteredApps.length != 0;
     }
+
+    /**
+     * Clones the target set without any actual targets. Used only when continuing a gesture after
+     * the actual recents animation has finished.
+     */
+    public RecentsAnimationTargets cloneWithoutTargets() {
+        return new RecentsAnimationTargets(new RemoteAnimationTargetCompat[0],
+                new RemoteAnimationTargetCompat[0], homeContentInsets, minimizedHomeBounds);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 517501a..465d464 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -25,12 +25,12 @@
 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.icons.IconProvider;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -48,11 +48,13 @@
 @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);
 
-    private final List<TaskVisualsChangeListener> mThumbnailChangeListeners = new ArrayList<>();
+    private final List<TaskThumbnailChangeListener> mThumbnailChangeListeners = new ArrayList<>();
     private final Context mContext;
 
     private final RecentTasksList mTaskList;
@@ -67,10 +69,8 @@
                 new KeyguardManagerCompat(context), ActivityManagerWrapper.getInstance());
         mIconCache = new TaskIconCache(context, looper);
         mThumbnailCache = new TaskThumbnailCache(context, looper);
-
         ActivityManagerWrapper.getInstance().registerTaskStackListener(this);
-        IconProvider.registerIconChangeListener(context,
-                this::onPackageIconChanged, MAIN_EXECUTOR.getHandler());
+        setupPackageListener();
     }
 
     public TaskIconCache getIconCache() {
@@ -183,40 +183,45 @@
         }
     }
 
-    private void onPackageIconChanged(String pkg, UserHandle user) {
-        mIconCache.invalidateCacheEntries(pkg, user);
-        for (int i = mThumbnailChangeListeners.size() - 1; i >= 0; i--) {
-            mThumbnailChangeListeners.get(i).onTaskIconChanged(pkg, user);
-        }
+    public void onOverviewShown(boolean fromHome, String tag) {
+        SystemUiProxy.INSTANCE.get(mContext).onOverviewShown(fromHome, tag);
     }
 
-    /**
-     * Adds a listener for visuals changes
-     */
-    public void addThumbnailChangeListener(TaskVisualsChangeListener listener) {
+    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);
     }
 
-    /**
-     * Removes a previously added listener
-     */
-    public void removeThumbnailChangeListener(TaskVisualsChangeListener listener) {
+    public void removeThumbnailChangeListener(TaskThumbnailChangeListener listener) {
         mThumbnailChangeListeners.remove(listener);
     }
 
-    /**
-     * Listener for receiving various task properties changes
-     */
-    public interface TaskVisualsChangeListener {
+    public interface TaskThumbnailChangeListener {
 
-        /**
-         * Called whn the task thumbnail changes
-         */
         Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData);
-
-        /**
-         * Called when the icon for a task changes
-         */
-        void onTaskIconChanged(String pkg, UserHandle user);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
deleted file mode 100644
index e3e8ace..0000000
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ /dev/null
@@ -1,173 +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.quickstep;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_INITIALIZED;
-
-import android.content.Intent;
-import android.util.Log;
-
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-
-public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAnimationListener {
-
-    private RecentsAnimationController mController;
-    private RecentsAnimationCallbacks mCallbacks;
-    private RecentsAnimationTargets mTargets;
-    // Temporary until we can hook into gesture state events
-    private GestureState mLastGestureState;
-
-    /**
-     * Preloads the recents animation.
-     */
-    public void preloadRecentsAnimation(Intent intent) {
-        // Pass null animation handler to indicate this start is for preloading
-        UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
-                .startRecentsActivity(intent, null, null, null, null));
-    }
-
-    /**
-     * Starts a new recents animation for the activity with the given {@param intent}.
-     */
-    @UiThread
-    public RecentsAnimationCallbacks startRecentsAnimation(GestureState gestureState,
-            Intent intent, RecentsAnimationCallbacks.RecentsAnimationListener listener) {
-        // Notify if recents animation is still running
-        if (mController != null) {
-            String msg = "New recents animation started before old animation completed";
-            if (FeatureFlags.IS_DOGFOOD_BUILD) {
-                throw new IllegalArgumentException(msg);
-            } else {
-                Log.e("TaskAnimationManager", msg, new Exception());
-            }
-        }
-        // But force-finish it anyways
-        finishRunningRecentsAnimation(false /* toHome */);
-
-        final BaseActivityInterface activityInterface = gestureState.getActivityInterface();
-        mLastGestureState = gestureState;
-        mCallbacks = new RecentsAnimationCallbacks(activityInterface.shouldMinimizeSplitScreen());
-        mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
-            @Override
-            public void onRecentsAnimationStart(RecentsAnimationController controller,
-                    RecentsAnimationTargets targets) {
-                mController = controller;
-                mTargets = targets;
-            }
-
-            @Override
-            public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
-                if (thumbnailData != null) {
-                    // If a screenshot is provided, switch to the screenshot before cleaning up
-                    activityInterface.switchRunningTaskViewToScreenshot(thumbnailData,
-                            () -> cleanUpRecentsAnimation(thumbnailData));
-                } else {
-                    cleanUpRecentsAnimation(null /* canceledThumbnail */);
-                }
-            }
-
-            @Override
-            public void onRecentsAnimationFinished(RecentsAnimationController controller) {
-                cleanUpRecentsAnimation(null /* canceledThumbnail */);
-            }
-        });
-        mCallbacks.addListener(gestureState);
-        mCallbacks.addListener(listener);
-        UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance()
-                .startRecentsActivity(intent, null, mCallbacks, null, null));
-        gestureState.setState(STATE_RECENTS_ANIMATION_INITIALIZED);
-        return mCallbacks;
-    }
-
-    /**
-     * Continues the existing running recents animation for a new gesture.
-     */
-    public RecentsAnimationCallbacks continueRecentsAnimation(GestureState gestureState) {
-        mCallbacks.removeListener(mLastGestureState);
-        mLastGestureState = gestureState;
-        mCallbacks.addListener(gestureState);
-        return mCallbacks;
-    }
-
-    /**
-     * Finishes the running recents animation.
-     */
-    public void finishRunningRecentsAnimation(boolean toHome) {
-        if (mController != null) {
-            mCallbacks.notifyAnimationCanceled();
-            Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), toHome
-                    ? mController::finishAnimationToHome
-                    : mController::finishAnimationToApp);
-            cleanUpRecentsAnimation(null /* canceledThumbnail */);
-        }
-    }
-
-    /**
-     * Used to notify a listener of the current recents animation state (used if the listener was
-     * not yet added to the callbacks at the point that the listener callbacks would have been
-     * made).
-     */
-    public void notifyRecentsAnimationState(
-            RecentsAnimationCallbacks.RecentsAnimationListener listener) {
-        if (isRecentsAnimationRunning()) {
-            listener.onRecentsAnimationStart(mController, mTargets);
-        }
-        // TODO: Do we actually need to report canceled/finished?
-    }
-
-    /**
-     * @return whether there is a recents animation running.
-     */
-    public boolean isRecentsAnimationRunning() {
-        return mController != null;
-    }
-
-    /**
-     * Cleans up the recents animation entirely.
-     */
-    private void cleanUpRecentsAnimation(ThumbnailData canceledThumbnail) {
-        // Clean up the screenshot if necessary
-        if (mController != null && canceledThumbnail != null) {
-            mController.cleanupScreenshot();
-        }
-
-        // Release all the target leashes
-        if (mTargets != null) {
-            mTargets.release();
-        }
-
-        // Remove gesture state from callbacks
-        if (mCallbacks != null && mLastGestureState != null) {
-            mCallbacks.removeListener(mLastGestureState);
-        }
-
-        mController = null;
-        mCallbacks = null;
-        mTargets = null;
-        mLastGestureState = null;
-    }
-
-    public void dump() {
-        // TODO
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index e590aea..289a129 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -15,64 +15,67 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.FastBitmapDrawable.newIcon;
-import static com.android.launcher3.uioverrides.QuickstepLauncher.GO_LOW_RAM_RECENTS_ENABLED;
+import static com.android.launcher3.uioverrides.RecentsUiFactory.GO_LOW_RAM_RECENTS_ENABLED;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
-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.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.os.UserHandle;
-import android.util.SparseArray;
+import android.util.LruCache;
 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.icons.BitmapInfo;
-import com.android.launcher3.icons.IconProvider;
-import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.util.TaskKeyLruCache;
 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 Context mContext;
-    private final TaskKeyLruCache<TaskCacheEntry> mIconCache;
-    private final SparseArray<BitmapInfo> mDefaultIcons = new SparseArray<>();
-    private final IconProvider mIconProvider;
+    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());
+            }
+        }
+    };
 
     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);
-        mIconProvider = new IconProvider(context);
+        mIconCache = new TaskKeyLruCache<>(cacheSize, mClearActivityInfoOnEviction);
+        mContentDescriptionCache = new TaskKeyLruCache<>(cacheSize, mClearActivityInfoOnEviction);
+        mActivityInfoCache = new LruCache<>(cacheSize);
+        mIconLoader = new NormalizedIconLoader(context, mIconCache, mActivityInfoCache,
+                true /* disableColorExtraction */);
     }
 
     /**
@@ -93,14 +96,15 @@
         IconLoadRequest request = new IconLoadRequest(mBackgroundHandler) {
             @Override
             public void run() {
-                TaskCacheEntry entry = getCacheEntry(task);
+                Drawable icon = mIconLoader.getIcon(task);
+                String contentDescription = loadContentDescriptionInBackground(task);
                 if (isCanceled()) {
                     // We don't call back to the provided callback in this case
                     return;
                 }
                 MAIN_EXECUTOR.execute(() -> {
-                    task.icon = entry.icon;
-                    task.titleDescription = entry.contentDescription;
+                    task.icon = icon;
+                    task.titleDescription = contentDescription;
                     callback.accept(task);
                     onEnd();
                 });
@@ -112,99 +116,51 @@
 
     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 invalidateCacheEntries(String pkg, UserHandle handle) {
-        Utilities.postAsyncCallback(mBackgroundHandler,
-                () -> mIconCache.removeAll(key ->
-                        pkg.equals(key.getPackageName()) && handle.getIdentifier() == key.userId));
-    }
-
-    @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(
-                        mIconProvider.getIcon(activityInfo, UserHandle.of(key.userId)),
-                        key.userId,
-                        desc.getPrimaryColor(),
-                        activityInfo.applicationInfo.isInstantApp());
-                entry.icon = newIcon(mContext, bitmapInfo);
-            } else {
-                entry.icon = getDefaultIcon(key.userId);
+    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);
             }
         }
-
-        // 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 {
@@ -212,9 +168,4 @@
             super(handler, null);
         }
     }
-
-    private static class TaskCacheEntry {
-        public Drawable icon;
-        public String contentDescription = "";
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index e47df6c..3b50c26 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -27,9 +27,9 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.util.Preconditions;
-import com.android.quickstep.util.TaskKeyLruCache;
 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.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 
@@ -41,7 +41,7 @@
     private final Handler mBackgroundHandler;
 
     private final int mCacheSize;
-    private final TaskKeyLruCache<ThumbnailData> mCache;
+    private final ThumbnailCache mCache;
     private final HighResLoadingState mHighResLoadingState;
 
     public static class HighResLoadingState {
@@ -100,7 +100,7 @@
 
         Resources res = context.getResources();
         mCacheSize = res.getInteger(R.integer.recentsThumbnailCacheSize);
-        mCache = new TaskKeyLruCache<>(mCacheSize);
+        mCache = new ThumbnailCache(mCacheSize);
     }
 
     /**
@@ -223,4 +223,21 @@
             this.reducedResolution = reducedResolution;
         }
     }
+
+    private static class ThumbnailCache extends TaskKeyLruCache<ThumbnailData> {
+
+        public ThumbnailCache(int cacheSize) {
+            super(cacheSize);
+        }
+
+        /**
+         * Updates the cache entry if it is already present in the cache
+         */
+        public void updateIfAlreadyInCache(int taskId, ThumbnailData thumbnailData) {
+            ThumbnailData oldData = getCacheEntry(taskId);
+            if (oldData != null) {
+                putCacheEntry(taskId, thumbnailData);
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java b/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
index b1c72ce..fe37d60 100644
--- a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
+++ b/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
@@ -31,11 +31,6 @@
     private final BiPredicate<T, Boolean> mOnInitListener;
     private final ActivityTracker<T> mActivityTracker;
 
-    /**
-     * @param onInitListener a callback made when the activity is initialized. The callback should
-     *                       return true to continue receiving callbacks (ie. for if the activity is
-     *                       recreated).
-     */
     public ActivityInitListener(BiPredicate<T, Boolean> onInitListener,
             ActivityTracker<T> tracker) {
         mOnInitListener = onInitListener;
@@ -47,10 +42,6 @@
         return mOnInitListener.test(activity, alreadyOnHome);
     }
 
-    /**
-     * Registers the activity-created listener. If the activity is already created, then the
-     * callback provided in the constructor will be called synchronously.
-     */
     public void register() {
         mActivityTracker.schedule(this);
     }
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index 2e118b4..050bdff 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -26,7 +26,7 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
-import com.android.quickstep.SysUINavigationMode;
+import com.android.launcher3.config.FeatureFlags;
 
 import java.lang.annotation.Retention;
 
@@ -39,27 +39,12 @@
     @IntDef({MULTI_WINDOW_STRATEGY_HALF_SCREEN, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE})
     private @interface MultiWindowStrategy {}
 
-    /**
-     * The height for the swipe up motion
-     */
-    public static float getDefaultSwipeHeight(Context context, DeviceProfile dp) {
-        float swipeHeight = dp.allAppsCellHeightPx - dp.allAppsIconTextSizePx;
-        if (SysUINavigationMode.getMode(context) == SysUINavigationMode.Mode.NO_BUTTON) {
-            swipeHeight -= dp.getInsets().bottom;
-        }
-        return swipeHeight;
-    }
-
     public static void calculateLauncherTaskSize(Context context, DeviceProfile dp, Rect outRect) {
         float extraSpace;
         if (dp.isVerticalBarLayout()) {
             extraSpace = 0;
         } else {
-            Resources res = context.getResources();
-
-            extraSpace = getDefaultSwipeHeight(context, dp) + dp.verticalDragHandleSizePx
-                    + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size)
-                    + res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_bottom_padding);
+            extraSpace = dp.hotseatBarSizePx + dp.verticalDragHandleSizePx;
         }
         calculateTaskSize(context, dp, extraSpace, MULTI_WINDOW_STRATEGY_HALF_SCREEN, outRect);
     }
diff --git a/quickstep/src/com/android/quickstep/util/NavBarPosition.java b/quickstep/src/com/android/quickstep/util/NavBarPosition.java
deleted file mode 100644
index a4614de..0000000
--- a/quickstep/src/com/android/quickstep/util/NavBarPosition.java
+++ /dev/null
@@ -1,127 +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.quickstep.util;
-
-import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.view.Gravity;
-import android.view.Surface;
-
-import com.android.launcher3.graphics.RotationMode;
-import com.android.launcher3.util.DefaultDisplay;
-import com.android.quickstep.SysUINavigationMode;
-
-/**
- * Utility class to check nav bar position.
- */
-public class NavBarPosition {
-
-    public static RotationMode ROTATION_LANDSCAPE = new RotationMode(-90) {
-        @Override
-        public void mapRect(int left, int top, int right, int bottom, Rect out) {
-            out.left = top;
-            out.top = right;
-            out.right = bottom;
-            out.bottom = left;
-        }
-
-        @Override
-        public void mapInsets(Context context, Rect insets, Rect out) {
-            // If there is a display cutout, the top insets in portrait would also include the
-            // cutout, which we will get as the left inset in landscape. Using the max of left and
-            // top allows us to cover both cases (with or without cutout).
-            if (SysUINavigationMode.getMode(context) == NO_BUTTON) {
-                out.top = Math.max(insets.top, insets.left);
-                out.bottom = Math.max(insets.right, insets.bottom);
-                out.left = out.right = 0;
-            } else {
-                out.top = Math.max(insets.top, insets.left);
-                out.bottom = insets.right;
-                out.left = insets.bottom;
-                out.right = 0;
-            }
-        }
-    };
-
-    public static RotationMode ROTATION_SEASCAPE = new RotationMode(90) {
-        @Override
-        public void mapRect(int left, int top, int right, int bottom, Rect out) {
-            out.left = bottom;
-            out.top = left;
-            out.right = top;
-            out.bottom = right;
-        }
-
-        @Override
-        public void mapInsets(Context context, Rect insets, Rect out) {
-            if (SysUINavigationMode.getMode(context) == NO_BUTTON) {
-                out.top = Math.max(insets.top, insets.right);
-                out.bottom = Math.max(insets.left, insets.bottom);
-                out.left = out.right = 0;
-            } else {
-                out.top = Math.max(insets.top, insets.right);
-                out.bottom = insets.left;
-                out.right = insets.bottom;
-                out.left = 0;
-            }
-        }
-
-        @Override
-        public int toNaturalGravity(int absoluteGravity) {
-            int horizontalGravity = absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
-            int verticalGravity = absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK;
-
-            if (horizontalGravity == Gravity.RIGHT) {
-                horizontalGravity = Gravity.LEFT;
-            } else if (horizontalGravity == Gravity.LEFT) {
-                horizontalGravity = Gravity.RIGHT;
-            }
-
-            if (verticalGravity == Gravity.TOP) {
-                verticalGravity = Gravity.BOTTOM;
-            } else if (verticalGravity == Gravity.BOTTOM) {
-                verticalGravity = Gravity.TOP;
-            }
-
-            return ((absoluteGravity & ~Gravity.HORIZONTAL_GRAVITY_MASK)
-                    & ~Gravity.VERTICAL_GRAVITY_MASK)
-                    | horizontalGravity | verticalGravity;
-        }
-    };
-
-    private final SysUINavigationMode.Mode mMode;
-    private final int mDisplayRotation;
-
-    public NavBarPosition(SysUINavigationMode.Mode mode, DefaultDisplay.Info info) {
-        mMode = mode;
-        mDisplayRotation = info.rotation;
-    }
-
-    public boolean isRightEdge() {
-        return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_90;
-    }
-
-    public boolean isLeftEdge() {
-        return mMode != NO_BUTTON && mDisplayRotation == Surface.ROTATION_270;
-    }
-
-    public RotationMode getRotationMode() {
-        return isLeftEdge() ? ROTATION_SEASCAPE
-                : (isRightEdge() ? ROTATION_LANDSCAPE : RotationMode.NORMAL);
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java
deleted file mode 100644
index d87feec..0000000
--- a/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java
+++ /dev/null
@@ -1,124 +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.quickstep.util;
-
-import android.util.Log;
-
-import com.android.systemui.shared.recents.model.Task.TaskKey;
-
-import java.util.LinkedHashMap;
-import java.util.function.Predicate;
-
-/**
- * A simple LRU cache for task key entries
- * @param <V> The type of the value
- */
-public class TaskKeyLruCache<V> {
-
-    private final MyLinkedHashMap<V> mMap;
-
-    public TaskKeyLruCache(int maxSize) {
-        mMap = new MyLinkedHashMap<>(maxSize);
-    }
-
-    /**
-     * Removes all entries from the cache
-     */
-    public synchronized void evictAll() {
-        mMap.clear();
-    }
-
-    /**
-     * Removes a particular entry from the cache
-     */
-    public synchronized void remove(TaskKey key) {
-        mMap.remove(key.id);
-    }
-
-    /**
-     * Removes all entries matching keyCheck
-     */
-    public synchronized void removeAll(Predicate<TaskKey> keyCheck) {
-        mMap.entrySet().removeIf(e -> keyCheck.test(e.getValue().mKey));
-    }
-
-    /**
-     * Gets the entry if it is still valid
-     */
-    public synchronized V getAndInvalidateIfModified(TaskKey key) {
-        Entry<V> entry = mMap.get(key.id);
-
-        if (entry != null && entry.mKey.windowingMode == key.windowingMode
-                && entry.mKey.lastActiveTime == key.lastActiveTime) {
-            return entry.mValue;
-        } else {
-            remove(key);
-            return null;
-        }
-    }
-
-    /**
-     * Adds an entry to the cache, optionally evicting the last accessed entry
-     */
-    public final synchronized void put(TaskKey key, V value) {
-        if (key != null && value != null) {
-            mMap.put(key.id, new Entry<>(key, value));
-        } else {
-            Log.e("TaskKeyCache", "Unexpected null key or value: " + key + ", " + value);
-        }
-    }
-
-    /**
-     * Updates the cache entry if it is already present in the cache
-     */
-    public synchronized void updateIfAlreadyInCache(int taskId, V data) {
-        Entry<V> entry = mMap.get(taskId);
-        if (entry != null) {
-            entry.mValue = data;
-        }
-    }
-
-    private static class Entry<V> {
-
-        final TaskKey mKey;
-        V mValue;
-
-        Entry(TaskKey key, V value) {
-            mKey = key;
-            mValue = value;
-        }
-
-        @Override
-        public int hashCode() {
-            return mKey.id;
-        }
-    }
-
-    private static class MyLinkedHashMap<V> extends LinkedHashMap<Integer, Entry<V>> {
-
-        private final int mMaxSize;
-
-        MyLinkedHashMap(int maxSize) {
-            super(0, 0.75f, true /* accessOrder */);
-            mMaxSize = maxSize;
-        }
-
-        @Override
-        protected boolean removeEldestEntry(Entry<Integer, TaskKeyLruCache.Entry<V>> eldest) {
-            return size() > mMaxSize;
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
index 0e591ca..26e9eaf 100644
--- a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
+++ b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
@@ -38,12 +38,12 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.uioverrides.states.OverviewState;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
-import com.android.quickstep.util.LayoutUtils;
 
 /**
  * Scrim used for all-apps and shelf in Overview
@@ -163,7 +163,7 @@
                 int hotseatSize = dp.hotseatBarSizePx + dp.getInsets().bottom
                         + hotseatPadding.bottom + hotseatPadding.top;
                 float dragHandleTop =
-                        Math.min(hotseatSize, LayoutUtils.getDefaultSwipeHeight(context, dp));
+                        Math.min(hotseatSize, OverviewState.getDefaultSwipeHeight(context, dp));
                 mDragHandleProgress =  1 - (dragHandleTop / mShiftRange);
             }
             mTopOffset = dp.getInsets().top - mShelfOffset;
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index ca81343..aa5fce1 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -31,9 +31,6 @@
 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;
@@ -102,7 +99,7 @@
         }
 
         mOrderSensitiveRules = RuleChain.outerRule(new NavigationModeSwitchRule(mLauncher))
-                .around(new FailureWatcher(mDevice));
+                        .around(new FailureWatcher(mDevice));
 
         mOtherLauncherActivity = context.getPackageManager().queryIntentActivities(
                 getHomeIntentInPackage(context),
@@ -133,11 +130,6 @@
     @NavigationModeSwitch
     @Test
     public void goToOverviewFromHome() {
-        // b/142828227
-        if (android.os.Build.MODEL.contains("Cuttlefish") && TestHelpers.isInLauncherProcess() &&
-                (RUN_FLAFOR & (PLATFORM_PRESUBMIT | UNBUNDLED_PRESUBMIT)) != 0) {
-            return;
-        }
         mDevice.pressHome();
         assertTrue("Fallback Launcher not visible", mDevice.wait(Until.hasObject(By.pkg(
                 mOtherLauncherActivity.packageName)), WAIT_TIME_MS));
@@ -148,11 +140,6 @@
     @NavigationModeSwitch
     @Test
     public void goToOverviewFromApp() {
-        // b/142828227
-        if (android.os.Build.MODEL.contains("Cuttlefish") && TestHelpers.isInLauncherProcess() &&
-                (RUN_FLAFOR & (PLATFORM_PRESUBMIT | UNBUNDLED_PRESUBMIT)) != 0) {
-            return;
-        }
         startAppFastAndWaitForRecentTask(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
 
         mLauncher.getBackground().switchToOverview();
@@ -175,7 +162,7 @@
             }
             result[0] = f.apply(activity);
             return true;
-        }).get(), DEFAULT_UI_TIMEOUT, mLauncher);
+        }).get(), DEFAULT_UI_TIMEOUT);
         return (T) result[0];
     }
 
@@ -187,16 +174,11 @@
     @NavigationModeSwitch
     @Test
     public void testOverview() {
-        // b/142828227
-        if (android.os.Build.MODEL.contains("Cuttlefish") && TestHelpers.isInLauncherProcess() &&
-                (RUN_FLAFOR & (PLATFORM_PRESUBMIT | UNBUNDLED_PRESUBMIT)) != 0) {
-            return;
-        }
         startAppFastAndWaitForRecentTask(getAppPackageName());
         startAppFastAndWaitForRecentTask(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
         startTestActivity(2);
         Wait.atMost("Expected three apps in the task list",
-                () -> mLauncher.getRecentTasks().size() >= 3, DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
+                () -> mLauncher.getRecentTasks().size() >= 3, DEFAULT_ACTIVITY_TIMEOUT);
 
         BaseOverview overview = mLauncher.getBackground().switchToOverview();
         executeOnRecents(recents ->
@@ -255,8 +237,7 @@
     private void startAppFastAndWaitForRecentTask(String packageName) {
         startAppFast(packageName);
         Wait.atMost("Expected app in task list",
-                () -> containsRecentTaskWithPackage(packageName), DEFAULT_ACTIVITY_TIMEOUT,
-                mLauncher);
+                () -> containsRecentTaskWithPackage(packageName), DEFAULT_ACTIVITY_TIMEOUT);
     }
 
     private boolean containsRecentTaskWithPackage(String packageName) {
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index fa4c7b9..c2197ab 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -30,7 +30,6 @@
 import android.content.pm.PackageManager;
 import android.util.Log;
 
-import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.UiDevice;
 
 import com.android.launcher3.tapl.LauncherInstrumentation;
@@ -81,13 +80,7 @@
             Mode mode = description.getAnnotation(NavigationModeSwitch.class).mode();
             return new Statement() {
                 private void assertTrue(String message, boolean condition) {
-                    if (mLauncher.getDevice().hasObject(By.textStartsWith(""))) {
-                        // The condition above is "screen is not empty". We are not treating
-                        // "Screen is empty" as an anomaly here. It's an acceptable state when
-                        // Launcher just starts under instrumentation.
-                        mLauncher.checkForAnomaly();
-                    }
-                    if (!condition) {
+                    if(!condition) {
                         final AssertionError assertionError = new AssertionError(message);
                         FailureWatcher.onError(mLauncher.getDevice(), description, assertionError);
                         throw assertionError;
diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
index d60fa1e..f5b9b7e 100644
--- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
@@ -18,9 +18,6 @@
 
 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;
 
@@ -28,7 +25,6 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.util.RaceConditionReproducer;
 import com.android.quickstep.NavigationModeSwitchRule.Mode;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 428e647..d270d76 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -248,33 +248,33 @@
     @Test
     @NavigationModeSwitch
     @PortraitLandscape
-    @Ignore // b/143285809
+    @Ignore("Temporarily disabled b/140252765")
     public void testQuickSwitchFromApp() throws Exception {
+        startAppFast(getAppPackageName());
         startTestActivity(2);
-        startTestActivity(3);
-        startTestActivity(4);
+        String calculatorPackage = resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR);
+        startAppFast(calculatorPackage);
 
         Background background = getAndAssertBackground();
         background.quickSwitchToPreviousApp();
         assertTrue("The first app we should have quick switched to is not running",
-                isTestActivityRunning(3));
+                isTestActivityRunning("TestActivity2"));
 
         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.",
-                    isTestActivityRunning(4));
+                    mDevice.wait(Until.hasObject(By.pkg(calculatorPackage)), DEFAULT_UI_TIMEOUT));
         } else {
             assertTrue("The second app we should have quick switched to is not running",
-                    isTestActivityRunning(2));
+                    isTestActivityRunning("Test Pin Item"));
         }
         getAndAssertBackground();
     }
 
-    private boolean isTestActivityRunning(int activityNumber) {
-        return mDevice.wait(Until.hasObject(By.pkg(getAppPackageName())
-                        .text("TestActivity" + activityNumber)),
+    private boolean isTestActivityRunning(String activityLabel) {
+        return mDevice.wait(Until.hasObject(By.pkg(getAppPackageName()).text(activityLabel)),
                 DEFAULT_UI_TIMEOUT);
     }
 
@@ -285,7 +285,7 @@
         startTestActivity(2);
         mLauncher.pressHome().quickSwitchToPreviousApp();
         assertTrue("The most recent task is not running after quick switching from home",
-                isTestActivityRunning(2));
+                isTestActivityRunning("TestActivity2"));
         getAndAssertBackground();
     }
 }
diff --git a/res/anim/slide_in_right.xml b/res/anim/slide_in_right.xml
new file mode 100644
index 0000000..55d3e54
--- /dev/null
+++ b/res/anim/slide_in_right.xml
@@ -0,0 +1,9 @@
+<?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/attrs.xml b/res/values/attrs.xml
index de17eb7..7be584e 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -115,6 +115,8 @@
         <attr name="numFolderColumns" format="integer" />
         <!-- numHotseatIcons defaults to numColumns, if not specified -->
         <attr name="numHotseatIcons" format="integer" />
+        <!-- numAllAppsColumns defaults to numColumns, if not specified -->
+        <attr name="numAllAppsColumns" format="integer" />
         <attr name="defaultLayoutId" format="reference" />
         <attr name="demoModeLayoutId" format="reference" />
     </declare-styleable>
@@ -130,6 +132,12 @@
         <attr name="iconTextSize" format="float" />
         <!-- If true, this display option is used to determine the default grid -->
         <attr name="canBeDefault" format="boolean" />
+
+        <!-- The following values are only enabled if grid is supported. -->
+        <!-- allAppsIconSize defaults to iconSize, if not specified -->
+        <attr name="allAppsIconSize" format="float" />
+        <!-- allAppsIconTextSize defaults to iconTextSize, if not specified -->
+        <attr name="allAppsIconTextSize" format="float" />
     </declare-styleable>
 
     <declare-styleable name="CellLayout">
diff --git a/res/values/config.xml b/res/values/config.xml
index 2a1f6f7..0387184 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -1,5 +1,5 @@
 <resources>
-    <!-- Miscellaneous -->
+<!-- Miscellaneous -->
     <bool name="config_largeHeap">false</bool>
     <bool name="is_tablet">false</bool>
     <bool name="is_large_tablet">false</bool>
@@ -21,10 +21,10 @@
     <!-- String representing the fragment class for settings activity.-->
     <string name="settings_fragment_name" translatable="false">com.android.launcher3.settings.SettingsActivity$LauncherSettingsFragment</string>
 
-    <!-- DragController -->
+<!-- DragController -->
     <item type="id" name="drag_event_parity" />
 
-    <!-- AllApps & Launcher transitions -->
+<!-- AllApps & Launcher transitions -->
     <!-- Out of 100, the percent to shrink the workspace during spring loaded mode. -->
     <integer name="config_workspaceSpringLoadShrinkPercentage">90</integer>
 
@@ -34,7 +34,7 @@
     <!-- View tag key used to store SpringAnimation data. -->
     <item type="id" name="spring_animation_tag" />
 
-    <!-- Workspace -->
+<!-- Workspace -->
     <!-- The duration (in ms) of the fade animation on the object outlines, used when
          we are dragging objects around on the home screen. -->
     <integer name="config_dragOutlineFadeTime">900</integer>
@@ -57,20 +57,29 @@
     <!-- The duration of the caret animation -->
     <integer name="config_caretAnimationDuration">200</integer>
 
-    <!-- Hotseat -->
+<!-- Hotseat -->
     <bool name="hotseat_transpose_layout_with_orientation">true</bool>
 
     <!-- Various classes overriden by projects/build flavors. -->
     <string name="app_filter_class" translatable="false"></string>
+    <string name="icon_provider_class" translatable="false"></string>
+    <string name="drawable_factory_class" translatable="false"></string>
     <string name="user_event_dispatcher_class" translatable="false"></string>
     <string name="stats_log_manager_class" translatable="false"></string>
     <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>
 
+    <!-- Package name of the default wallpaper picker. -->
+    <string name="wallpaper_picker_package" translatable="false"></string>
+
+    <!-- Whitelisted package to retrieve packagename for badge. Can be empty. -->
+    <string name="shortcutinfo_badgepkg_whitelist" translatable="false"></string>
+
     <!-- View ID to use for QSB widget -->
     <item type="id" name="qsb_widget" />
 
@@ -88,12 +97,7 @@
     <integer name="config_popupArrowOpenCloseDuration">40</integer>
     <integer name="config_removeNotificationViewDuration">300</integer>
 
-    <!-- Default packages -->
-    <string name="wallpaper_picker_package" translatable="false"></string>
-    <string name="calendar_component_name" translatable="false"></string>
-    <string name="clock_component_name" translatable="false"></string>
-
-    <!-- Accessibility actions -->
+<!-- Accessibility actions -->
     <item type="id" name="action_remove" />
     <item type="id" name="action_uninstall" />
     <item type="id" name="action_reconfigure" />
@@ -108,10 +112,10 @@
     <item type="id" name="action_dismiss_notification" />
     <item type="id" name="action_remote_action_shortcut" />
 
-    <!-- QSB IDs. DO not change -->
+<!-- QSB IDs. DO not change -->
     <item type="id" name="search_container_workspace" />
     <item type="id" name="search_container_all_apps" />
 
-    <!-- Recents -->
+<!-- Recents -->
     <item type="id" name="overview_panel"/>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index dec8939..9d9c2e8 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -102,11 +102,9 @@
     <string name="app_info_drop_target_label">App info</string>
     <!-- Label for install drop target. [CHAR_LIMIT=20] -->
     <string name="install_drop_target_label">Install</string>
+
     <!-- Label for install dismiss prediction. -->
     <string translatable="false" name="dismiss_prediction_label">Dismiss prediction</string>
-    <!-- Label for pinning predicted app. -->
-    <string name="pin_prediction" translatable="false">Pin Prediction</string>
-
 
     <!-- Permissions: -->
     <skip />
diff --git a/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java b/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
index 5b6d94d..32eb2ec 100644
--- a/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
+++ b/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
@@ -202,14 +202,15 @@
             CacheEntry entry = mCache.get(new ComponentKey(componentName, user));
             if (entry == null) {
                 entry = new CacheEntry();
-                entry.bitmap = getDefaultIcon(user);
+                getDefaultIcon(user).applyTo(entry);
             }
             return entry;
         }
 
         public void addCache(ComponentName key, String title) {
             CacheEntry entry = new CacheEntry();
-            entry.bitmap = BitmapInfo.of(newIcon(), Color.RED);
+            entry.icon = newIcon();
+            entry.color = Color.RED;
             entry.title = title;
             mCache.put(new ComponentKey(key, Process.myUserHandle()), entry);
         }
diff --git a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
index 69c5b00..81b9043 100644
--- a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -2,13 +2,13 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNull;
 
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.icons.BitmapInfo;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -43,7 +43,7 @@
     public void testCacheUpdate_update_apps() throws Exception {
         // Clear all icons from apps list so that its easy to check what was updated
         for (AppInfo info : allAppsList.data) {
-            info.bitmap = BitmapInfo.LOW_RES_INFO;
+            info.iconBitmap = null;
         }
 
         executeTaskForTest(newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, "app1"));
@@ -56,9 +56,9 @@
         assertFalse(allAppsList.data.isEmpty());
         for (AppInfo info : allAppsList.data) {
             if (info.componentName.getPackageName().equals("app1")) {
-                assertFalse(info.bitmap.isNullOrLowRes());
+                assertNotNull(info.iconBitmap);
             } else {
-                assertTrue(info.bitmap.isNullOrLowRes());
+                assertNull(info.iconBitmap);
             }
         }
     }
@@ -85,10 +85,10 @@
         for (ItemInfo info : bgDataModel.itemsIdMap) {
             if (updates.contains(info.id)) {
                 assertEquals(NEW_LABEL_PREFIX + info.id, info.title);
-                assertFalse(((WorkspaceItemInfo) info).bitmap.isNullOrLowRes());
+                assertNotNull(((WorkspaceItemInfo) info).iconBitmap);
             } else {
                 assertNotSame(NEW_LABEL_PREFIX + info.id, info.title);
-                assertTrue(((WorkspaceItemInfo) info).bitmap.isNullOrLowRes());
+                assertNull(((WorkspaceItemInfo) info).iconBitmap);
             }
         }
     }
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 382bfdf..b28077f 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.ApiWrapper;
+import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.ViewCache;
@@ -81,39 +81,19 @@
     protected StatsLogManager mStatsLogManager;
     protected SystemUiController mSystemUiController;
 
-
-    public static final int ACTIVITY_STATE_STARTED = 1 << 0;
-    public static final int ACTIVITY_STATE_RESUMED = 1 << 1;
-
+    private static final int ACTIVITY_STATE_STARTED = 1 << 0;
+    private static final int ACTIVITY_STATE_RESUMED = 1 << 1;
     /**
-     * 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
+     * State flag indicating if the user is active or the actitvity when to background as a result
      * of user action.
      * @see #isUserActive()
      */
-    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;
+    private static final int ACTIVITY_STATE_USER_ACTIVE = 1 << 2;
 
     @Retention(SOURCE)
     @IntDef(
             flag = true,
-            value = {ACTIVITY_STATE_STARTED,
-                    ACTIVITY_STATE_RESUMED,
-                    ACTIVITY_STATE_DEFERRED_RESUMED,
-                    ACTIVITY_STATE_WINDOW_FOCUSED,
-                    ACTIVITY_STATE_USER_ACTIVE,
-                    ACTIVITY_STATE_TRANSITION_ACTIVE})
+            value = {ACTIVITY_STATE_STARTED, ACTIVITY_STATE_RESUMED, ACTIVITY_STATE_USER_ACTIVE})
     public @interface ActivityFlags{}
 
     @ActivityFlags
@@ -166,19 +146,19 @@
 
     @Override
     protected void onStart() {
-        addActivityFlags(ACTIVITY_STATE_STARTED);
+        mActivityFlags |= ACTIVITY_STATE_STARTED;
         super.onStart();
     }
 
     @Override
     protected void onResume() {
-        addActivityFlags(ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_USER_ACTIVE);
+        mActivityFlags |= ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_USER_ACTIVE;
         super.onResume();
     }
 
     @Override
     protected void onUserLeaveHint() {
-        removeActivityFlags(ACTIVITY_STATE_USER_ACTIVE);
+        mActivityFlags &= ~ACTIVITY_STATE_USER_ACTIVE;
         super.onUserLeaveHint();
     }
 
@@ -192,7 +172,7 @@
 
     @Override
     protected void onStop() {
-        removeActivityFlags(ACTIVITY_STATE_STARTED | ACTIVITY_STATE_USER_ACTIVE);
+        mActivityFlags &= ~ACTIVITY_STATE_STARTED & ~ACTIVITY_STATE_USER_ACTIVE;
         mForceInvisible = 0;
         super.onStop();
 
@@ -203,7 +183,7 @@
 
     @Override
     protected void onPause() {
-        removeActivityFlags(ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_DEFERRED_RESUMED);
+        mActivityFlags &= ~ACTIVITY_STATE_RESUMED;
         super.onPause();
 
         // Reset the overridden sysui flags used for the task-swipe launch animation, we do this
@@ -213,17 +193,6 @@
         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;
     }
@@ -239,22 +208,6 @@
         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);
     }
@@ -280,7 +233,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;
@@ -307,7 +260,7 @@
 
     @Override
     public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
-        if (!ApiWrapper.dumpActivity(this, writer)) {
+        if (!UiFactory.dumpActivity(this, writer)) {
             super.dump(prefix, fd, writer, args);
         }
     }
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 772eb00..d24de8e 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -57,7 +57,7 @@
     private ActionMode mCurrentActionMode;
     protected boolean mIsSafeModeEnabled;
 
-    private Runnable mOnStartCallback;
+    private OnStartCallback mOnStartCallback;
 
     private int mThemeRes = R.style.AppTheme;
 
@@ -226,7 +226,7 @@
         super.onStart();
 
         if (mOnStartCallback != null) {
-            mOnStartCallback.run();
+            mOnStartCallback.onActivityStart(this);
             mOnStartCallback = null;
         }
     }
@@ -238,12 +238,8 @@
         mRotationListener.disable();
     }
 
-    public void runOnceOnStart(Runnable action) {
-        mOnStartCallback = action;
-    }
-
-    public void clearRunOnceOnStartCallback() {
-        mOnStartCallback = null;
+    public <T extends BaseDraggingActivity> void setOnStartCallback(OnStartCallback<T> callback) {
+        mOnStartCallback = callback;
     }
 
     protected void onDeviceProfileInitiated() {
@@ -262,4 +258,12 @@
     }
 
     protected abstract void reapplyUi();
+
+    /**
+     * Callback for listening for onStart
+     */
+    public interface OnStartCallback<T extends BaseDraggingActivity> {
+
+        void onActivityStart(T activity);
+    }
 }
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index e6f8a85..7adb6a4 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3;
 
-import static com.android.launcher3.FastBitmapDrawable.newIcon;
-import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
 
 import android.animation.Animator;
@@ -47,6 +45,7 @@
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.graphics.PreloadIconDrawable;
@@ -216,7 +215,6 @@
         cancelDotScaleAnim();
         mDotParams.scale = 0f;
         mForceHideDot = false;
-        setBackground(null);
     }
 
     private void cancelDotScaleAnim() {
@@ -289,8 +287,9 @@
     }
 
     private void applyIconAndLabel(ItemInfoWithIcon info) {
-        FastBitmapDrawable iconDrawable = newIcon(getContext(), info);
-        mDotParams.color = IconPalette.getMutedColor(info.bitmap.color, 0.54f);
+        FastBitmapDrawable iconDrawable = DrawableFactory.INSTANCE.get(getContext())
+                .newIcon(getContext(), info);
+        mDotParams.color = IconPalette.getMutedColor(info.iconColor, 0.54f);
 
         setIcon(iconDrawable);
         setText(info.title);
@@ -497,8 +496,7 @@
         // Text should be visible everywhere but the hotseat.
         Object tag = getParent() instanceof FolderIcon ? ((View) getParent()).getTag() : getTag();
         ItemInfo info = tag instanceof ItemInfo ? (ItemInfo) tag : null;
-        return info == null || (info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT
-                && info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION);
+        return info == null || info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT;
     }
 
     public void setTextVisibility(boolean visible) {
@@ -569,7 +567,8 @@
                     preloadDrawable = (PreloadIconDrawable) mIcon;
                     preloadDrawable.setLevel(progressLevel);
                 } else {
-                    preloadDrawable = newPendingIcon(getContext(), info);
+                    preloadDrawable = DrawableFactory.INSTANCE.get(getContext())
+                            .newPendingIcon(getContext(), info);
                     preloadDrawable.setLevel(progressLevel);
                     setIcon(preloadDrawable);
                 }
@@ -666,7 +665,7 @@
             mDisableRelayout = true;
 
             // Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
-            info.bitmap.icon.prepareToDraw();
+            info.iconBitmap.prepareToDraw();
 
             if (info instanceof AppInfo) {
                 applyFromApplicationInfo((AppInfo) info);
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index a35f598..736142f 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -25,8 +25,6 @@
 import android.util.DisplayMetrics;
 import android.view.Surface;
 
-import androidx.annotation.Nullable;
-
 import com.android.launcher3.CellLayout.ContainerType;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.icons.DotRenderer;
@@ -36,8 +34,6 @@
 public class DeviceProfile {
 
     public final InvariantDeviceProfile inv;
-    // IDP with no grid override values.
-    @Nullable private final InvariantDeviceProfile originalIdp;
 
     // Device properties
     public final boolean isTablet;
@@ -138,11 +134,10 @@
     public DotRenderer mDotRendererAllApps;
 
     public DeviceProfile(Context context, InvariantDeviceProfile inv,
-            InvariantDeviceProfile originalIDP, Point minSize, Point maxSize,
+            Point minSize, Point maxSize,
             int width, int height, boolean isLandscape, boolean isMultiWindowMode) {
 
         this.inv = inv;
-        this.originalIdp = inv;
         this.isLandscape = isLandscape;
         this.isMultiWindowMode = isMultiWindowMode;
 
@@ -234,19 +229,6 @@
             // Recalculate the available dimensions using the new hotseat size.
             updateAvailableDimensions(dm, res);
         }
-
-        if (originalIDP != null) {
-            // Grid size change should not affect All Apps UI, so we use the original profile
-            // measurements here.
-            DeviceProfile originalProfile = isLandscape
-                    ? originalIDP.landscapeProfile
-                    : originalIDP.portraitProfile;
-            allAppsIconSizePx = originalProfile.iconSizePx;
-            allAppsIconTextSizePx = originalProfile.iconTextSizePx;
-            allAppsCellHeightPx = originalProfile.allAppsCellHeightPx;
-            allAppsIconDrawablePaddingPx = originalProfile.iconDrawablePaddingOriginalPx;
-            allAppsCellWidthPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx;
-        }
         updateWorkspacePadding();
 
         // This is done last, after iconSizePx is calculated above.
@@ -259,8 +241,8 @@
 
     public DeviceProfile copy(Context context) {
         Point size = new Point(availableWidthPx, availableHeightPx);
-        return new DeviceProfile(context, inv, originalIdp, size, size, widthPx, heightPx,
-                isLandscape, isMultiWindowMode);
+        return new DeviceProfile(context, inv, size, size, widthPx, heightPx, isLandscape,
+                isMultiWindowMode);
     }
 
     public DeviceProfile getMultiWindowProfile(Context context, Point mwSize) {
@@ -271,8 +253,8 @@
         // In multi-window mode, we can have widthPx = availableWidthPx
         // and heightPx = availableHeightPx because Launcher uses the InvariantDeviceProfiles'
         // widthPx and heightPx values where it's needed.
-        DeviceProfile profile = new DeviceProfile(context, inv, originalIdp, mwSize, mwSize,
-                mwSize.x, mwSize.y, isLandscape, true);
+        DeviceProfile profile = new DeviceProfile(context, inv, mwSize, mwSize, mwSize.x, mwSize.y,
+                isLandscape, true);
 
         // If there isn't enough vertical cell padding with the labels displayed, hide the labels.
         float workspaceCellPaddingY = profile.getCellSize().y - profile.iconSizePx
@@ -356,10 +338,18 @@
         }
         cellWidthPx = iconSizePx + iconDrawablePaddingPx;
 
-        allAppsIconSizePx = iconSizePx;
-        allAppsIconTextSizePx = iconTextSizePx;
-        allAppsIconDrawablePaddingPx = iconDrawablePaddingPx;
-        allAppsCellHeightPx = getCellSize().y;
+        // All apps
+        if (allAppsHasDifferentNumColumns()) {
+            allAppsIconSizePx = ResourceUtils.pxFromDp(inv.allAppsIconSize, dm);
+            allAppsIconTextSizePx = Utilities.pxFromSp(inv.allAppsIconTextSize, dm);
+            allAppsCellHeightPx = getCellSize(inv.numAllAppsColumns, inv.numAllAppsColumns).y;
+            allAppsIconDrawablePaddingPx = iconDrawablePaddingOriginalPx;
+        } else {
+            allAppsIconSizePx = iconSizePx;
+            allAppsIconTextSizePx = iconTextSizePx;
+            allAppsIconDrawablePaddingPx = iconDrawablePaddingPx;
+            allAppsCellHeightPx = getCellSize().y;
+        }
         allAppsCellWidthPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx;
 
         if (isVerticalBarLayout()) {
diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java
index 5091684..a90025e 100644
--- a/src/com/android/launcher3/FastBitmapDrawable.java
+++ b/src/com/android/launcher3/FastBitmapDrawable.java
@@ -20,7 +20,6 @@
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 
 import android.animation.ObjectAnimator;
-import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -36,7 +35,6 @@
 import android.util.Property;
 import android.util.SparseArray;
 
-import com.android.launcher3.graphics.PlaceHolderIconDrawable;
 import com.android.launcher3.icons.BitmapInfo;
 
 public class FastBitmapDrawable extends Drawable {
@@ -100,6 +98,10 @@
         this(info.icon, info.color);
     }
 
+    public FastBitmapDrawable(ItemInfoWithIcon info) {
+        this(info.iconBitmap, info.iconColor);
+    }
+
     protected FastBitmapDrawable(Bitmap b, int iconColor) {
         this(b, iconColor, false);
     }
@@ -363,7 +365,7 @@
         }
 
         @Override
-        public FastBitmapDrawable newDrawable() {
+        public Drawable newDrawable() {
             return new FastBitmapDrawable(mBitmap, mIconColor, mIsDisabled);
         }
 
@@ -372,37 +374,4 @@
             return 0;
         }
     }
-
-    /**
-     * Interface to be implemented by custom {@link BitmapInfo} to handle drawable construction
-     */
-    public interface Factory {
-
-        /**
-         * Called to create a new drawable
-         */
-        FastBitmapDrawable newDrawable();
-    }
-
-    /**
-     * Returns a FastBitmapDrawable with the icon.
-     */
-    public static FastBitmapDrawable newIcon(Context context, ItemInfoWithIcon info) {
-        FastBitmapDrawable drawable = newIcon(context, info.bitmap);
-        drawable.setIsDisabled(info.isDisabled());
-        return drawable;
-    }
-
-    /**
-     * Creates a drawable for the provided BitmapInfo
-     */
-    public static FastBitmapDrawable newIcon(Context context, BitmapInfo info) {
-        if (info instanceof Factory) {
-            return ((Factory) info).newDrawable();
-        } else if (info.isLowRes()) {
-            return new PlaceHolderIconDrawable(info, context);
-        } else {
-            return new FastBitmapDrawable(info);
-        }
-    }
 }
diff --git a/src/com/android/launcher3/IconProvider.java b/src/com/android/launcher3/IconProvider.java
new file mode 100644
index 0000000..0f006f7
--- /dev/null
+++ b/src/com/android/launcher3/IconProvider.java
@@ -0,0 +1,29 @@
+package com.android.launcher3;
+
+import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
+
+import android.content.pm.LauncherActivityInfo;
+import android.graphics.drawable.Drawable;
+
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.ResourceBasedOverride;
+
+public class IconProvider implements ResourceBasedOverride {
+
+    public static MainThreadInitializedObject<IconProvider> INSTANCE =
+            forOverride(IconProvider.class, R.string.icon_provider_class);
+
+    public IconProvider() { }
+
+    public String getSystemStateForPackage(String systemState, String packageName) {
+        return systemState;
+    }
+
+    /**
+     * @param flattenDrawable true if the caller does not care about the specification of the
+     *                        original icon as long as the flattened version looks the same.
+     */
+    public Drawable getIcon(LauncherActivityInfo info, int iconDpi, boolean flattenDrawable) {
+        return info.getIcon(iconDpi);
+    }
+}
diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
index 21359f1..0b79dd2 100644
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -22,7 +22,6 @@
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.BroadcastReceiver;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
@@ -41,11 +40,9 @@
 import android.util.Log;
 import android.util.Pair;
 
-import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.GraphicsUtils;
 import com.android.launcher3.icons.LauncherIcons;
@@ -242,6 +239,11 @@
         return info == null ? null : (WorkspaceItemInfo) info.getItemInfo().first;
     }
 
+    public static WorkspaceItemInfo fromActivityInfo(LauncherActivityInfo info, Context context) {
+        return (WorkspaceItemInfo)
+                new PendingInstallShortcutInfo(info, context).getItemInfo().first;
+    }
+
     public static void queueShortcut(ShortcutInfo info, Context context) {
         queuePendingShortcutInfo(new PendingInstallShortcutInfo(info, context), context);
     }
@@ -317,10 +319,10 @@
     private static class PendingInstallShortcutInfo {
 
         final boolean isActivity;
-        @Nullable final ShortcutInfo shortcutInfo;
-        @Nullable final AppWidgetProviderInfo providerInfo;
+        final ShortcutInfo shortcutInfo;
+        final AppWidgetProviderInfo providerInfo;
 
-        @Nullable final Intent data;
+        final Intent data;
         final Context mContext;
         final Intent launchIntent;
         final String label;
@@ -350,12 +352,7 @@
             shortcutInfo = null;
             providerInfo = null;
 
-            String packageName = info.getComponentName().getPackageName();
-            data = new Intent();
-            data.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent().setComponent(
-                    new ComponentName(packageName, "")).setPackage(packageName));
-            data.putExtra(Intent.EXTRA_SHORTCUT_NAME, info.getLabel());
-
+            data = null;
             user = info.getUser();
             mContext = context;
 
@@ -449,26 +446,21 @@
                 // This name is only used for comparisons and notifications, so fall back to activity
                 // name if not supplied
                 String name = ensureValidName(mContext, launchIntent, label).toString();
-                Bitmap icon = data == null ? null
-                        : data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
-                Intent.ShortcutIconResource iconResource = data == null ? null
-                    : data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
+                Bitmap icon = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON);
+                Intent.ShortcutIconResource iconResource =
+                    data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE);
 
                 // Only encode the parameters which are supported by the API.
                 JSONStringer json = new JSONStringer()
                     .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);
-                    if (iconByteArray != null) {
-                        json = json.key(ICON_KEY).value(
-                                Base64.encodeToString(
-                                        iconByteArray, 0, iconByteArray.length, Base64.DEFAULT));
-                    }
+                    json = json.key(ICON_KEY).value(
+                            Base64.encodeToString(
+                                    iconByteArray, 0, iconByteArray.length, Base64.DEFAULT));
                 }
                 if (iconResource != null) {
                     json = json.key(ICON_RESOURCE_NAME_KEY).value(iconResource.resourceName);
@@ -484,20 +476,14 @@
 
         public Pair<ItemInfo, Object> getItemInfo() {
             if (isActivity) {
-                WorkspaceItemInfo si = createWorkspaceItemInfo(data, user,
+                WorkspaceItemInfo si = createWorkspaceItemInfo(data,
                         LauncherAppState.getInstance(mContext));
                 si.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
                 si.status |= WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
                 return Pair.create(si, null);
             } else if (shortcutInfo != null) {
                 WorkspaceItemInfo itemInfo = new WorkspaceItemInfo(shortcutInfo, mContext);
-                if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
-                    fetchAndUpdateShortcutIconAsync(mContext, itemInfo, shortcutInfo, true);
-                } else {
-                    LauncherIcons li = LauncherIcons.obtain(mContext);
-                    itemInfo.bitmap = li.createShortcutIcon(shortcutInfo);
-                    li.recycle();
-                }
+                fetchAndUpdateShortcutIconAsync(mContext, itemInfo, shortcutInfo, true);
                 return Pair.create(itemInfo, shortcutInfo);
             } else if (providerInfo != null) {
                 LauncherAppWidgetProviderInfo info = LauncherAppWidgetProviderInfo
@@ -513,7 +499,7 @@
                 return Pair.create(widgetInfo, providerInfo);
             } else {
                 WorkspaceItemInfo itemInfo =
-                        createWorkspaceItemInfo(data, user, LauncherAppState.getInstance(mContext));
+                        createWorkspaceItemInfo(data, LauncherAppState.getInstance(mContext));
                 return Pair.create(itemInfo, null);
             }
         }
@@ -631,8 +617,7 @@
         return new PendingInstallShortcutInfo(info, original.mContext);
     }
 
-    private static WorkspaceItemInfo createWorkspaceItemInfo(Intent data, UserHandle user,
-            LauncherAppState app) {
+    private static WorkspaceItemInfo createWorkspaceItemInfo(Intent data, LauncherAppState app) {
         if (data == null) {
             Log.e(TAG, "Can't construct WorkspaceItemInfo with null data");
             return null;
@@ -649,7 +634,10 @@
         }
 
         final WorkspaceItemInfo info = new WorkspaceItemInfo();
-        info.user = user;
+
+        // 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();
 
         BitmapInfo iconInfo = null;
         LauncherIcons li = LauncherIcons.obtain(app.getContext());
@@ -667,7 +655,7 @@
         if (iconInfo == null) {
             iconInfo = app.getIconCache().getDefaultIcon(info.user);
         }
-        info.bitmap = iconInfo;
+        info.applyFrom(iconInfo);
 
         info.title = Utilities.trim(name);
         info.contentDescription = app.getContext().getPackageManager()
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 9d87152..310a9e9 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -58,7 +58,6 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 
 public class InvariantDeviceProfile {
 
@@ -104,6 +103,8 @@
     public int iconBitmapSize;
     public int fillResIconDpi;
     public float iconTextSize;
+    public float allAppsIconSize;
+    public float allAppsIconTextSize;
 
     private SparseArray<TypedValue> mExtraAttrs;
 
@@ -144,6 +145,8 @@
         iconTextSize = p.iconTextSize;
         numHotseatIcons = p.numHotseatIcons;
         numAllAppsColumns = p.numAllAppsColumns;
+        allAppsIconSize = p.allAppsIconSize;
+        allAppsIconTextSize = p.allAppsIconTextSize;
         defaultLayoutId = p.defaultLayoutId;
         demoModeLayoutId = p.demoModeLayoutId;
         mExtraAttrs = p.mExtraAttrs;
@@ -188,11 +191,54 @@
         Point smallestSize = new Point(displayInfo.smallestSize);
         Point largestSize = new Point(displayInfo.largestSize);
 
+        ArrayList<DisplayOption> allOptions = getPredefinedDeviceProfiles(context, gridName);
         // This guarantees that width < height
         float minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y),
                 displayInfo.metrics);
         float minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y),
                 displayInfo.metrics);
+        // Sort the profiles based on the closeness to the device size
+        Collections.sort(allOptions, (a, b) ->
+                Float.compare(dist(minWidthDps, minHeightDps, a.minWidthDps, a.minHeightDps),
+                        dist(minWidthDps, minHeightDps, b.minWidthDps, b.minHeightDps)));
+        DisplayOption interpolatedDisplayOption =
+                invDistWeightedInterpolate(minWidthDps,  minHeightDps, allOptions);
+
+        GridOption closestProfile = allOptions.get(0).grid;
+        numRows = closestProfile.numRows;
+        numColumns = closestProfile.numColumns;
+        numHotseatIcons = closestProfile.numHotseatIcons;
+        defaultLayoutId = closestProfile.defaultLayoutId;
+        demoModeLayoutId = closestProfile.demoModeLayoutId;
+        numFolderRows = closestProfile.numFolderRows;
+        numFolderColumns = closestProfile.numFolderColumns;
+        numAllAppsColumns = closestProfile.numAllAppsColumns;
+
+        mExtraAttrs = closestProfile.extraAttrs;
+
+        if (!closestProfile.name.equals(gridName)) {
+            Utilities.getPrefs(context).edit()
+                    .putString(KEY_IDP_GRID_NAME, closestProfile.name).apply();
+        }
+
+        iconSize = interpolatedDisplayOption.iconSize;
+        iconShapePath = getIconShapePath(context);
+        landscapeIconSize = interpolatedDisplayOption.landscapeIconSize;
+        iconBitmapSize = ResourceUtils.pxFromDp(iconSize, displayInfo.metrics);
+        iconTextSize = interpolatedDisplayOption.iconTextSize;
+        fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
+
+        if (Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)) {
+            allAppsIconSize = interpolatedDisplayOption.allAppsIconSize;
+            allAppsIconTextSize = interpolatedDisplayOption.allAppsIconTextSize;
+        } else {
+            allAppsIconSize = iconSize;
+            allAppsIconTextSize = iconTextSize;
+        }
+
+        // If the partner customization apk contains any grid overrides, apply them
+        // Supported overrides: numRows, numColumns, iconSize
+        applyPartnerDeviceProfileOverrides(context, displayInfo.metrics);
 
         Point realSize = new Point(displayInfo.realSize);
         // The real size never changes. smallSide and largeSide will remain the
@@ -200,64 +246,10 @@
         int smallSide = Math.min(realSize.x, realSize.y);
         int largeSide = Math.max(realSize.x, realSize.y);
 
-        // We want a list of all options as well as the list of filtered options. This allows us
-        // to have a consistent UI for areas that the grid size change should not affect
-        // ie. All Apps should be consistent between grid sizes.
-        ArrayList<DisplayOption> allOptions = new ArrayList<>();
-        ArrayList<DisplayOption> filteredOptions = new ArrayList<>();
-        getPredefinedDeviceProfiles(context, gridName, filteredOptions, allOptions);
-
-        if (allOptions.isEmpty() && filteredOptions.isEmpty()) {
-            throw new RuntimeException("No display option with canBeDefault=true");
-        }
-
-        // Sort the profiles based on the closeness to the device size
-        Comparator<DisplayOption> comparator = (a, b) -> Float.compare(dist(minWidthDps,
-                minHeightDps, a.minWidthDps, a.minHeightDps),
-                dist(minWidthDps, minHeightDps, b.minWidthDps, b.minHeightDps));
-
-        // Calculate the device profiles as if there is no grid override.
-        Collections.sort(allOptions, comparator);
-        DisplayOption interpolatedDisplayOption =
-                invDistWeightedInterpolate(minWidthDps,  minHeightDps, allOptions);
-        initGridOption(context, allOptions, interpolatedDisplayOption, displayInfo.metrics);
-
-        // Create IDP with no grid override values.
-        InvariantDeviceProfile originalIDP = new InvariantDeviceProfile(this);
-        originalIDP.landscapeProfile = new DeviceProfile(context, this, null, smallestSize,
-                largestSize, largeSide, smallSide, true /* isLandscape */,
-                false /* isMultiWindowMode */);
-        originalIDP.portraitProfile = new DeviceProfile(context, this, null, smallestSize,
-                largestSize, smallSide, largeSide, false /* isLandscape */,
-                false /* isMultiWindowMode */);
-
-        if (filteredOptions.isEmpty()) {
-            filteredOptions = allOptions;
-
-            landscapeProfile = originalIDP.landscapeProfile;
-            portraitProfile = originalIDP.portraitProfile;
-        } else {
-            Collections.sort(filteredOptions, comparator);
-            interpolatedDisplayOption =
-                    invDistWeightedInterpolate(minWidthDps, minHeightDps, filteredOptions);
-
-            initGridOption(context, filteredOptions, interpolatedDisplayOption,
-                    displayInfo.metrics);
-            numAllAppsColumns = originalIDP.numAllAppsColumns;
-
-            landscapeProfile = new DeviceProfile(context, this, originalIDP, smallestSize,
-                    largestSize, largeSide, smallSide, true /* isLandscape */,
-                    false /* isMultiWindowMode */);
-            portraitProfile = new DeviceProfile(context, this, originalIDP, smallestSize,
-                    largestSize, smallSide, largeSide, false /* isLandscape */,
-                    false /* isMultiWindowMode */);
-        }
-
-        GridOption closestProfile = filteredOptions.get(0).grid;
-        if (!closestProfile.name.equals(gridName)) {
-            Utilities.getPrefs(context).edit()
-                    .putString(KEY_IDP_GRID_NAME, closestProfile.name).apply();
-        }
+        landscapeProfile = new DeviceProfile(context, this, smallestSize, largestSize,
+                largeSide, smallSide, true /* isLandscape */, false /* isMultiWindowMode */);
+        portraitProfile = new DeviceProfile(context, this, smallestSize, largestSize,
+                smallSide, largeSide, false /* isLandscape */, false /* isMultiWindowMode */);
 
         // We need to ensure that there is enough extra space in the wallpaper
         // for the intended parallax effects
@@ -275,33 +267,6 @@
         return closestProfile.name;
     }
 
-    private void initGridOption(Context context, ArrayList<DisplayOption> options,
-            DisplayOption displayOption, DisplayMetrics metrics) {
-        GridOption closestProfile = options.get(0).grid;
-        numRows = closestProfile.numRows;
-        numColumns = closestProfile.numColumns;
-        numHotseatIcons = closestProfile.numHotseatIcons;
-        defaultLayoutId = closestProfile.defaultLayoutId;
-        demoModeLayoutId = closestProfile.demoModeLayoutId;
-        numFolderRows = closestProfile.numFolderRows;
-        numFolderColumns = closestProfile.numFolderColumns;
-        numAllAppsColumns = numColumns;
-
-        mExtraAttrs = closestProfile.extraAttrs;
-
-        iconSize = displayOption.iconSize;
-        iconShapePath = getIconShapePath(context);
-        landscapeIconSize = displayOption.landscapeIconSize;
-        iconBitmapSize = ResourceUtils.pxFromDp(iconSize, metrics);
-        iconTextSize = displayOption.iconTextSize;
-        fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
-
-        // If the partner customization apk contains any grid overrides, apply them
-        // Supported overrides: numRows, numColumns, iconSize
-        applyPartnerDeviceProfileOverrides(context, metrics);
-    }
-
-
     @Nullable
     public TypedValue getAttrValue(int attr) {
         return mExtraAttrs == null ? null : mExtraAttrs.get(attr);
@@ -379,13 +344,7 @@
         }
     }
 
-    /**
-     * @param gridName The current grid name.
-     * @param filteredOptionsOut List filled with all the filtered options based on gridName.
-     * @param allOptionsOut List filled with all the options that can be the default option.
-     */
-    static void getPredefinedDeviceProfiles(Context context, String gridName,
-            ArrayList<DisplayOption> filteredOptionsOut, ArrayList<DisplayOption> allOptionsOut) {
+    static ArrayList<DisplayOption> getPredefinedDeviceProfiles(Context context, String gridName) {
         ArrayList<DisplayOption> profiles = new ArrayList<>();
         try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
             final int depth = parser.getDepth();
@@ -412,19 +371,26 @@
             throw new RuntimeException(e);
         }
 
+        ArrayList<DisplayOption> filteredProfiles = new ArrayList<>();
         if (!TextUtils.isEmpty(gridName)) {
             for (DisplayOption option : profiles) {
                 if (gridName.equals(option.grid.name)) {
-                    filteredOptionsOut.add(option);
+                    filteredProfiles.add(option);
                 }
             }
         }
-
-        for (DisplayOption option : profiles) {
-            if (option.canBeDefault) {
-                allOptionsOut.add(option);
+        if (filteredProfiles.isEmpty()) {
+            // No grid found, use the default options
+            for (DisplayOption option : profiles) {
+                if (option.canBeDefault) {
+                    filteredProfiles.add(option);
+                }
             }
         }
+        if (filteredProfiles.isEmpty()) {
+            throw new RuntimeException("No display option with canBeDefault=true");
+        }
+        return filteredProfiles;
     }
 
     private int getLauncherIconDensity(int requiredSize) {
@@ -548,6 +514,8 @@
 
         private final int numHotseatIcons;
 
+        private final int numAllAppsColumns;
+
         private final int defaultLayoutId;
         private final int demoModeLayoutId;
 
@@ -570,6 +538,8 @@
                     R.styleable.GridDisplayOption_numFolderRows, numRows);
             numFolderColumns = a.getInt(
                     R.styleable.GridDisplayOption_numFolderColumns, numColumns);
+            numAllAppsColumns = a.getInt(
+                    R.styleable.GridDisplayOption_numAllAppsColumns, numColumns);
 
             a.recycle();
 
@@ -589,6 +559,8 @@
         private float iconSize;
         private float iconTextSize;
         private float landscapeIconSize;
+        private float allAppsIconSize;
+        private float allAppsIconTextSize;
 
         DisplayOption(GridOption grid, Context context, AttributeSet attrs) {
             this.grid = grid;
@@ -607,6 +579,10 @@
                     iconSize);
             iconTextSize = a.getFloat(R.styleable.ProfileDisplayOption_iconTextSize, 0);
 
+            allAppsIconSize = a.getFloat(R.styleable.ProfileDisplayOption_allAppsIconSize,
+                    iconSize);
+            allAppsIconTextSize = a.getFloat(R.styleable.ProfileDisplayOption_allAppsIconTextSize,
+                    iconTextSize);
             a.recycle();
         }
 
@@ -621,14 +597,18 @@
         private DisplayOption multiply(float w) {
             iconSize *= w;
             landscapeIconSize *= w;
+            allAppsIconSize *= w;
             iconTextSize *= w;
+            allAppsIconTextSize *= w;
             return this;
         }
 
         private DisplayOption add(DisplayOption p) {
             iconSize += p.iconSize;
             landscapeIconSize += p.landscapeIconSize;
+            allAppsIconSize += p.allAppsIconSize;
             iconTextSize += p.iconTextSize;
+            allAppsIconTextSize += p.allAppsIconTextSize;
             return this;
         }
     }
diff --git a/src/com/android/launcher3/ItemInfoWithIcon.java b/src/com/android/launcher3/ItemInfoWithIcon.java
index 1941455..1550bb0 100644
--- a/src/com/android/launcher3/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/ItemInfoWithIcon.java
@@ -16,6 +16,10 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON;
+
+import android.graphics.Bitmap;
+
 import com.android.launcher3.icons.BitmapInfo;
 
 /**
@@ -26,9 +30,14 @@
     public static final String TAG = "ItemInfoDebug";
 
     /**
-     * The bitmap for the application icon
+     * A bitmap version of the application icon.
      */
-    public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO;
+    public Bitmap iconBitmap;
+
+    /**
+     * Dominant color in the {@link #iconBitmap}.
+     */
+    public int iconColor;
 
     /**
      * Indicates that the icon is disabled due to safe mode restrictions.
@@ -97,7 +106,8 @@
 
     protected ItemInfoWithIcon(ItemInfoWithIcon info) {
         super(info);
-        bitmap = info.bitmap;
+        iconBitmap = info.iconBitmap;
+        iconColor = info.iconColor;
         runtimeStatusFlags = info.runtimeStatusFlags;
     }
 
@@ -110,7 +120,12 @@
      * Indicates whether we're using a low res icon
      */
     public boolean usingLowResIcon() {
-        return bitmap.isLowRes();
+        return iconBitmap == LOW_RES_ICON;
+    }
+
+    public void applyFrom(BitmapInfo info) {
+        iconBitmap = info.icon;
+        iconColor = info.color;
     }
 
     /**
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 5a5f7c3..4b4d793 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -31,11 +31,8 @@
 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;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -61,7 +58,6 @@
 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;
@@ -86,8 +82,6 @@
 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;
@@ -102,7 +96,6 @@
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.folder.FolderGridOrganizer;
 import com.android.launcher3.folder.FolderIcon;
-import com.android.launcher3.folder.FolderNameProvider;
 import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.keyboard.CustomActionsPopup;
@@ -118,11 +111,11 @@
 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.touch.AllAppsSwipeController;
+import com.android.launcher3.testing.TestProtocol;
 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;
@@ -142,7 +135,6 @@
 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;
@@ -174,7 +166,6 @@
 import java.util.List;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
-import java.util.stream.Stream;
 
 /**
  * Default launcher application.
@@ -276,10 +267,6 @@
 
     private ArrayList<OnResumeCallback> mOnResumeCallbacks = new ArrayList<>();
 
-    // Used to notify when an activity launch has been deferred because launcher is not yet resumed
-    // TODO: See if we can remove this later
-    private Runnable mOnDeferredActivityLaunchCallback;
-
     private ViewOnDrawExecutor mPendingExecutor;
 
     private LauncherModel mModel;
@@ -303,11 +290,12 @@
      */
     private PendingRequestArgs mPendingRequestArgs;
     // Request id for any pending activity result
-    protected int mPendingActivityRequestCode = -1;
+    private int mPendingActivityRequestCode = -1;
 
     public ViewGroupFocusHelper mFocusHandler;
 
     private RotationHelper mRotationHelper;
+    private Runnable mCancelTouchController;
 
     final Handler mHandler = new Handler();
     private final Runnable mHandleDeferredResume = this::handleDeferredResume;
@@ -358,6 +346,7 @@
         mDragController = new DragController(this);
         mAllAppsController = new AllAppsTransitionController(this);
         mStateManager = new LauncherStateManager(this);
+        UiFactory.onCreate(this);
 
         mAppWidgetManager = AppWidgetManagerCompat.getInstance(this);
         mAppWidgetHost = new LauncherAppWidgetHost(this,
@@ -481,6 +470,7 @@
     @Override
     public void onEnterAnimationComplete() {
         super.onEnterAnimationComplete();
+        UiFactory.onEnterAnimationComplete(this);
         mAllAppsController.highlightWorkTabIfNecessary();
         mRotationHelper.setCurrentTransitionRequest(REQUEST_NONE);
     }
@@ -494,6 +484,7 @@
         }
 
         mOldConfig.setTo(newConfig);
+        UiFactory.onLauncherStateOrResumeChanged(this);
         super.onConfigurationChanged(newConfig);
     }
 
@@ -509,7 +500,7 @@
     public void reapplyUi() {
         if (supportsFakeLandscapeUI()) {
             mRotationMode = mStableDeviceProfile == null
-                    ? RotationMode.NORMAL : getFakeRotationMode(mDeviceProfile);
+                    ? RotationMode.NORMAL : UiFactory.getRotationMode(mDeviceProfile);
         }
         getRootView().dispatchInsets();
         getStateManager().reapplyState(true /* cancelCurrentAnimation */);
@@ -572,7 +563,7 @@
 
         if (supportsFakeLandscapeUI() && mDeviceProfile.isVerticalBarLayout()) {
             mStableDeviceProfile = mDeviceProfile.inv.portraitProfile;
-            mRotationMode = getFakeRotationMode(mDeviceProfile);
+            mRotationMode = UiFactory.getRotationMode(mDeviceProfile);
         } else {
             mStableDeviceProfile = null;
             mRotationMode = RotationMode.NORMAL;
@@ -615,10 +606,6 @@
         return mStateManager;
     }
 
-    public FolderNameProvider getFolderNameProvider() {
-        return new FolderNameProvider();
-    }
-
     @Override
     public <T extends View> T findViewById(int id) {
         return mLauncherView.findViewById(id);
@@ -942,6 +929,8 @@
         NotificationListener.removeNotificationsChangedListener();
         getStateManager().moveToRestState();
 
+        UiFactory.onLauncherStateOrResumeChanged(this);
+
         // Workaround for b/78520668, explicitly trim memory once UI is hidden
         onTrimMemory(TRIM_MEMORY_UI_HIDDEN);
     }
@@ -964,6 +953,7 @@
             logStopAndResume(Action.Command.RESUME);
             getUserEventDispatcher().startSession();
 
+            UiFactory.onLauncherStateOrResumeChanged(this);
             AppLaunchTracker.INSTANCE.get(this).onReturnedToHome();
 
             // Process any items that were added while Launcher was away.
@@ -978,17 +968,15 @@
 
             DiscoveryBounce.showForHomeIfNeeded(this);
 
-            onDeferredResumed();
-            addActivityFlags(ACTIVITY_STATE_DEFERRED_RESUMED);
-
+            if (mPendingActivityRequestCode != -1 && isInState(NORMAL)) {
+                UiFactory.resetPendingActivityResults(this, mPendingActivityRequestCode);
+            }
             mDeferredResumePending = false;
         } else {
             mDeferredResumePending = true;
         }
     }
 
-    protected void onDeferredResumed() { }
-
     private void logStopAndResume(int command) {
         int containerType = mStateManager.getState().containerType;
         if (containerType == ContainerType.WORKSPACE && mWorkspace != null) {
@@ -1042,14 +1030,12 @@
         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
@@ -1094,6 +1080,18 @@
         }
     }
 
+    @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) {
@@ -1157,6 +1155,7 @@
 
         // 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
@@ -1534,6 +1533,11 @@
         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.
@@ -1569,7 +1573,10 @@
         if (requestCode != -1) {
             mPendingActivityRequestCode = requestCode;
         }
-        super.startActivityForResult(intent, requestCode, options);
+        if (requestCode == -1
+                || !UiFactory.startActivityForResult(this, intent, requestCode, options)) {
+            super.startActivityForResult(intent, requestCode, options);
+        }
     }
 
     @Override
@@ -1578,11 +1585,14 @@
         if (requestCode != -1) {
             mPendingActivityRequestCode = requestCode;
         }
-        try {
-            super.startIntentSenderForResult(intent, requestCode,
-                    fillInIntent, flagsMask, flagsValues, extraFlags, options);
-        } catch (IntentSender.SendIntentException e) {
-            throw new ActivityNotFoundException();
+        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();
+            }
         }
     }
 
@@ -1876,10 +1886,7 @@
             // recents animation into launcher. Defer launching the activity until Launcher is
             // next resumed.
             addOnResumeCallback(() -> startActivitySafely(v, intent, item, sourceContainer));
-            if (mOnDeferredActivityLaunchCallback != null) {
-                mOnDeferredActivityLaunchCallback.run();
-                mOnDeferredActivityLaunchCallback = null;
-            }
+            UiFactory.clearSwipeSharedState(this, true /* finishAnimation */);
             return true;
         }
 
@@ -1920,6 +1927,7 @@
             // This clears all widget bitmaps from the widget tray
             // TODO(hyunyoungs)
         }
+        UiFactory.onTrimMemory(this, level);
     }
 
     @Override
@@ -1940,14 +1948,6 @@
     }
 
     /**
-     * Persistant callback which notifies when an activity launch is deferred because the activity
-     * was not yet resumed.
-     */
-    public void setOnDeferredActivityLaunchCallback(Runnable callback) {
-        mOnDeferredActivityLaunchCallback = callback;
-    }
-
-    /**
      * Implementation of the method from LauncherModel.Callbacks.
      */
     @Override
@@ -2638,40 +2638,16 @@
         return super.onKeyUp(keyCode, event);
     }
 
-    protected StateHandler[] createStateHandlers() {
-        return new StateHandler[] { getAllAppsController(), getWorkspace() };
+    public static Launcher getLauncher(Context context) {
+        return (Launcher) fromContext(context);
     }
 
-    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/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 79f4821..c717d1a 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -30,14 +30,12 @@
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.pm.InstallSessionTracker;
 import com.android.launcher3.pm.PackageInstallerCompat;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SecureSettingsObserver;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
@@ -59,7 +57,6 @@
 
     private final InstallSessionTracker mInstallSessionTracker;
     private final SimpleBroadcastReceiver mModelChangeReceiver;
-    private final SafeCloseable mCalendarChangeTracker;
 
     public static LauncherAppState getInstance(final Context context) {
         return INSTANCE.get(context);
@@ -95,10 +92,6 @@
         if (FeatureFlags.IS_DOGFOOD_BUILD) {
             mModelChangeReceiver.register(mContext, ACTION_FORCE_ROLOAD);
         }
-
-        mCalendarChangeTracker = IconProvider.registerIconChangeListener(mContext,
-                mModel::onAppIconChanged, MODEL_EXECUTOR.getHandler());
-
         // TODO: remove listener on terminate
         FeatureFlags.APP_SEARCH_IMPROVEMENTS.addChangeListener(context, mModel::forceReload);
         CustomWidgetManager.INSTANCE.get(mContext)
@@ -150,7 +143,6 @@
         mContext.unregisterReceiver(mModelChangeReceiver);
         mContext.getSystemService(LauncherApps.class).unregisterCallback(mModel);
         mInstallSessionTracker.unregister();
-        mCalendarChangeTracker.close();
         CustomWidgetManager.INSTANCE.get(mContext).setWidgetRefreshCallback(null);
 
         if (mNotificationDotsObserver != null) {
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 5ff5b04..fc2e953 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
-import android.content.Context;
 import android.content.Intent;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageInstaller;
@@ -32,7 +31,6 @@
 import android.util.Pair;
 
 import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
@@ -55,6 +53,7 @@
 import com.android.launcher3.pm.InstallSessionTracker;
 import com.android.launcher3.pm.PackageInstallInfo;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.PackageUserKey;
@@ -94,6 +93,10 @@
     private boolean mModelLoaded;
     public boolean isModelLoaded() {
         synchronized (mLock) {
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.LAUNCHER_DIDNT_INITIALIZE,
+                        "isModelLoaded: " + mModelLoaded + ", " + mLoaderTask);
+            }
             return mModelLoaded && mLoaderTask == null;
         }
     }
@@ -211,21 +214,9 @@
         enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, true));
     }
 
-    /**
-     * Called when the icon for an app changes, outside of package event
-     */
-    @WorkerThread
-    public void onAppIconChanged(String packageName, UserHandle user) {
-        // Update the icon for the calendar package
-        Context context = mApp.getContext();
-        onPackageChanged(packageName, user);
-
-        List<ShortcutInfo> pinnedShortcuts = DeepShortcutManager.getInstance(context)
-                .queryForPinnedShortcuts(packageName, user);
-        if (!pinnedShortcuts.isEmpty()) {
-            enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, pinnedShortcuts, user,
-                    false));
-        }
+    public void updatePinnedShortcuts(String packageName, List<ShortcutInfo> shortcuts,
+            UserHandle user) {
+        enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, false));
     }
 
     public void onBroadcastIntent(Intent intent) {
@@ -534,7 +525,7 @@
         updateAndBindWorkspaceItem(() -> {
             si.updateFromDeepShortcutInfo(info, mApp.getContext());
             LauncherIcons li = LauncherIcons.obtain(mApp.getContext());
-            si.bitmap = li.createShortcutIcon(info);
+            si.applyFrom(li.createShortcutIcon(info));
             li.recycle();
             return si;
         });
@@ -570,8 +561,7 @@
         if (args.length > 0 && TextUtils.equals(args[0], "--all")) {
             writer.println(prefix + "All apps list: size=" + mBgAllAppsList.data.size());
             for (AppInfo info : mBgAllAppsList.data) {
-                writer.println(prefix + "   title=\"" + info.title
-                        + "\" bitmapIcon=" + info.bitmap.icon
+                writer.println(prefix + "   title=\"" + info.title + "\" iconBitmap=" + info.iconBitmap
                         + " componentName=" + info.componentName.getPackageName());
             }
         }
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index ec307db..c509680 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -127,7 +127,6 @@
         public static final int CONTAINER_DESKTOP = -100;
         public static final int CONTAINER_HOTSEAT = -101;
         public static final int CONTAINER_PREDICTION = -102;
-        public static final int CONTAINER_HOTSEAT_PREDICTION = -103;
 
         static final String containerToString(int container) {
             switch (container) {
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index d2b447b..6e2626b 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -26,11 +26,9 @@
 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;
@@ -38,11 +36,14 @@
 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;
@@ -209,7 +210,7 @@
     }
 
     public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        return launcher.getOverviewScaleAndTranslationForNormalState();
+        return UiFactory.getOverviewScaleAndTranslationForNormalState(launcher);
     }
 
     public float getOverviewFullscreenProgress() {
diff --git a/src/com/android/launcher3/LauncherStateManager.java b/src/com/android/launcher3/LauncherStateManager.java
index daf270b..848e19f 100644
--- a/src/com/android/launcher3/LauncherStateManager.java
+++ b/src/com/android/launcher3/LauncherStateManager.java
@@ -24,8 +24,7 @@
 import android.animation.AnimatorSet;
 import android.os.Handler;
 import android.os.Looper;
-
-import androidx.annotation.IntDef;
+import android.util.Log;
 
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
@@ -33,12 +32,16 @@
 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
  *
@@ -143,7 +146,7 @@
 
     public StateHandler[] getStateHandlers() {
         if (mStateHandlers == null) {
-            mStateHandlers = mLauncher.createStateHandlers();
+            mStateHandlers = UiFactory.getStateHandler(mLauncher);
         }
         return mStateHandlers;
     }
@@ -411,6 +414,7 @@
             // 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);
@@ -431,6 +435,8 @@
             setRestState(null);
         }
 
+        UiFactory.onLauncherStateOrResumeChanged(mLauncher);
+
         for (int i = mListeners.size() - 1; i >= 0; i--) {
             mListeners.get(i).onStateTransitionComplete(state);
         }
@@ -438,6 +444,10 @@
         AccessibilityManagerCompat.sendStateEventToTest(mLauncher, state.ordinal);
     }
 
+    public void onWindowFocusChanged() {
+        UiFactory.onLauncherStateOrFocusChanged(mLauncher);
+    }
+
     public LauncherState getLastState() {
         return mLastStableState;
     }
diff --git a/src/com/android/launcher3/SessionCommitReceiver.java b/src/com/android/launcher3/SessionCommitReceiver.java
index e0c50e2..8dedc6c 100644
--- a/src/com/android/launcher3/SessionCommitReceiver.java
+++ b/src/com/android/launcher3/SessionCommitReceiver.java
@@ -71,13 +71,8 @@
 
         SessionInfo info = intent.getParcelableExtra(PackageInstaller.EXTRA_SESSION);
         UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER);
-        if (!PackageInstaller.ACTION_SESSION_COMMITTED.equals(intent.getAction())
-                || info == null || user == null) {
-            // Invalid intent.
-            return;
-        }
-
         PackageInstallerCompat packageInstallerCompat = PackageInstallerCompat.getInstance(context);
+
         if (TextUtils.isEmpty(info.getAppPackageName())
                 || info.getInstallReason() != PackageManager.INSTALL_REASON_USER
                 || packageInstallerCompat.promiseIconAddedForId(info.getSessionId())) {
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 2bec0ba..92f8069 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -63,7 +63,6 @@
 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
 import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.graphics.TintedDrawableSpan;
-import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
@@ -519,20 +518,19 @@
     }
 
     /**
-     * Returns the full drawable for info without any flattening or pre-processing.
-     *
+     * Returns the full drawable for {@param info}.
      * @param outObj this is set to the internal data associated with {@param info},
      *               eg {@link LauncherActivityInfo} or {@link ShortcutInfo}.
      */
     public static Drawable getFullDrawable(Launcher launcher, ItemInfo info, int width, int height,
-            Object[] outObj) {
+            boolean flattenDrawable, Object[] outObj) {
         LauncherAppState appState = LauncherAppState.getInstance(launcher);
         if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
             LauncherActivityInfo activityInfo = launcher.getSystemService(LauncherApps.class)
                     .resolveActivity(info.getIntent(), info.user);
             outObj[0] = activityInfo;
-            return activityInfo == null ? null : new IconProvider(launcher).getIconForUI(
-                    activityInfo, launcher.getDeviceProfile().inv.fillResIconDpi);
+            return (activityInfo != null) ? appState.getIconCache()
+                    .getFullResIcon(activityInfo, flattenDrawable) : null;
         } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
             if (info instanceof PendingAddShortcutInfo) {
                 ShortcutConfigActivityInfo activityInfo =
@@ -584,9 +582,9 @@
             }
             ShortcutInfo si = (ShortcutInfo) obj;
             LauncherIcons li = LauncherIcons.obtain(appState.getContext());
-            Bitmap badge = li.getShortcutInfoBadge(si, appState.getIconCache()).bitmap.icon;
+            Bitmap badge = li.getShortcutInfoBadge(si, appState.getIconCache()).iconBitmap;
             li.recycle();
-            float badgeSize = LauncherIcons.getBadgeSizeForIconSize(iconSize);
+            float badgeSize = iconSize * LauncherIcons.getBadgeSizeForIconSize(iconSize);
             float insetFraction = (iconSize - badgeSize) / iconSize;
             return new InsetDrawable(new FastBitmapDrawable(badge),
                     insetFraction, insetFraction, 0, 0);
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java
index 37b58d3..c5e74ef 100644
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ b/src/com/android/launcher3/WidgetPreviewLoader.java
@@ -23,19 +23,16 @@
 import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
 import android.os.CancellationSignal;
 import android.os.Process;
 import android.os.UserHandle;
-import android.util.ArrayMap;
 import android.util.Log;
 import android.util.LongSparseArray;
 import android.util.Pair;
 
 import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
 
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.UserManagerCompat;
@@ -81,9 +78,6 @@
     private final UserManagerCompat mUserManager;
     private final CacheDb mDb;
 
-    private final UserHandle mMyUser = Process.myUserHandle();
-    private final ArrayMap<UserHandle, Bitmap> mUserBadges = new ArrayMap<>();
-
     public WidgetPreviewLoader(Context context, IconCache iconCache) {
         mContext = context;
         mIconCache = iconCache;
@@ -92,51 +86,6 @@
     }
 
     /**
-     * Returns a drawable that can be used as a badge for the user or null.
-     */
-    @UiThread
-    public Drawable getBadgeForUser(UserHandle user, int badgeSize) {
-        if (mMyUser.equals(user)) {
-            return null;
-        }
-
-        Bitmap badgeBitmap = getUserBadge(user, badgeSize);
-        FastBitmapDrawable d = new FastBitmapDrawable(badgeBitmap);
-        d.setFilterBitmap(true);
-        d.setBounds(0, 0, badgeBitmap.getWidth(), badgeBitmap.getHeight());
-        return d;
-    }
-
-    private Bitmap getUserBadge(UserHandle user, int badgeSize) {
-        synchronized (mUserBadges) {
-            Bitmap badgeBitmap = mUserBadges.get(user);
-            if (badgeBitmap != null) {
-                return badgeBitmap;
-            }
-
-            final Resources res = mContext.getResources();
-            badgeBitmap = Bitmap.createBitmap(badgeSize, badgeSize, Bitmap.Config.ARGB_8888);
-
-            Drawable drawable = mContext.getPackageManager().getUserBadgedDrawableForDensity(
-                    new BitmapDrawable(res, badgeBitmap), user,
-                    new Rect(0, 0, badgeSize, badgeSize),
-                    0);
-            if (drawable instanceof BitmapDrawable) {
-                badgeBitmap = ((BitmapDrawable) drawable).getBitmap();
-            } else {
-                badgeBitmap.eraseColor(Color.TRANSPARENT);
-                Canvas c = new Canvas(badgeBitmap);
-                drawable.setBounds(0, 0, badgeSize, badgeSize);
-                drawable.draw(c);
-                c.setBitmap(null);
-            }
-
-            mUserBadges.put(user, badgeBitmap);
-            return badgeBitmap;
-        }
-    }
-
-    /**
      * Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be
      * called on UI thread
      *
@@ -157,8 +106,8 @@
 
     public void refresh() {
         mDb.clear();
-    }
 
+    }
     /**
      * The DB holds the generated previews for various components. Previews can also have different
      * sizes (landscape vs portrait).
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 431a149..eca5d12 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -1471,6 +1471,9 @@
 
     public DragView beginDragShared(View child, DragSource source, ItemInfo dragObject,
             DragPreviewProvider previewProvider, DragOptions dragOptions) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_CONTEXT_MENU, "beginDragShared");
+        }
         float iconScale = 1f;
         if (child instanceof BubbleTextView) {
             Drawable icon = ((BubbleTextView) child).getIcon();
diff --git a/src/com/android/launcher3/WorkspaceItemInfo.java b/src/com/android/launcher3/WorkspaceItemInfo.java
index be907e5..23795c5 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.ApiWrapper;
+import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.util.ContentWriter;
 
 import java.util.Arrays;
@@ -140,7 +140,7 @@
                 .put(Favorites.RESTORED, status);
 
         if (!usingLowResIcon()) {
-            writer.putIcon(bitmap, user);
+            writer.putIcon(iconBitmap, user);
         }
         if (iconResource != null) {
             writer.put(Favorites.ICON_PACKAGE, iconResource.packageName)
@@ -192,7 +192,7 @@
         }
         disabledMessage = shortcutInfo.getDisabledMessage();
 
-        Person[] persons = ApiWrapper.getPersons(shortcutInfo);
+        Person[] persons = UiFactory.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/WorkspaceLayoutManager.java b/src/com/android/launcher3/WorkspaceLayoutManager.java
index 0b9d602..ea2d4d0 100644
--- a/src/com/android/launcher3/WorkspaceLayoutManager.java
+++ b/src/com/android/launcher3/WorkspaceLayoutManager.java
@@ -39,8 +39,7 @@
     default void addInScreenFromBind(View child, ItemInfo info) {
         int x = info.cellX;
         int y = info.cellY;
-        if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
-                || info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+        if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
             int screenId = info.screenId;
             x = getHotseat().getCellXFromOrder(screenId);
             y = getHotseat().getCellYFromOrder(screenId);
@@ -84,8 +83,7 @@
         }
 
         final CellLayout layout;
-        if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
-                || container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+        if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
             layout = getHotseat();
 
             // Hide folder title in the hotseat
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index 7a7e1fe..40c6b5f 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -38,6 +38,7 @@
 import com.android.launcher3.LauncherStateManager.AnimationConfig;
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.graphics.WorkspaceAndHotseatScrim;
 
 /**
@@ -95,13 +96,14 @@
             propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, scaleInterpolator);
 
             if (!hotseat.getRotationMode().isTransposed) {
-                // Set the hotseat's pivot point to match the workspace's, so that it scales
-                // together. Since both hotseat and workspace can move, transform the point
-                // manually instead of using dragLayer.getDescendantCoordRelativeToSelf and
-                // related methods.
-                hotseat.setPivotY(mWorkspace.getPivotY() + mWorkspace.getTop() - hotseat.getTop());
-                hotseat.setPivotX(mWorkspace.getPivotX()
-                        + mWorkspace.getLeft() - hotseat.getLeft());
+                // Set the hotseat's pivot point to match the workspace's, so that it scales together.
+                DragLayer dragLayer = mLauncher.getDragLayer();
+                float[] workspacePivot =
+                        new float[]{ mWorkspace.getPivotX(), mWorkspace.getPivotY() };
+                dragLayer.getDescendantCoordRelativeToSelf(mWorkspace, workspacePivot);
+                dragLayer.mapCoordInSelfToDescendant(hotseat, workspacePivot);
+                hotseat.setPivotX(workspacePivot[0]);
+                hotseat.setPivotY(workspacePivot[1]);
             }
             float hotseatScale = hotseatScaleAndTranslation.scale;
             Interpolator hotseatScaleInterpolator = builder.getInterpolator(ANIM_HOTSEAT_SCALE,
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 08ce9c2..3836c9f 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -2,6 +2,8 @@
 
 import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT;
 import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.VERTICAL_SWIPE_INDICATOR;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE;
@@ -132,6 +134,15 @@
         } else {
             mLauncher.getSystemUiController().updateUiState(UI_STATE_ALL_APPS, 0);
         }
+
+        if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
+            // Translate hotseat with the shelf until reaching overview.
+            float overviewProgress = OVERVIEW.getVerticalProgress(mLauncher);
+            if (progress >= overviewProgress || mLauncher.isInState(BACKGROUND_APP)) {
+                float hotseatShift = (progress - overviewProgress) * mShiftRange;
+                mLauncher.getHotseat().setTranslationY(hotseatShift);
+            }
+        }
     }
 
     public float getProgress() {
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index f4b705e..4abdbef 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -110,10 +110,6 @@
             "FAKE_LANDSCAPE_UI", false,
             "Rotate launcher UI instead of using transposed layout");
 
-    public static final TogglableFlag FOLDER_NAME_SUGGEST = new TogglableFlag(
-            "FOLDER_NAME_SUGGEST", true,
-            "Suggests folder names instead of blank text.");
-
     public static final TogglableFlag APP_SEARCH_IMPROVEMENTS = new TogglableFlag(
             "APP_SEARCH_IMPROVEMENTS", false,
             "Adds localized title and keyword search and ranking");
@@ -124,16 +120,6 @@
     public static final TogglableFlag ENABLE_QUICK_CAPTURE_GESTURE = new TogglableFlag(
             "ENABLE_QUICK_CAPTURE_GESTURE", false, "Swipe from right to left to quick capture");
 
-    public static final TogglableFlag ASSISTANT_GIVES_LAUNCHER_FOCUS = new TogglableFlag(
-            "ASSISTANT_GIVES_LAUNCHER_FOCUS", false,
-            "Allow Launcher to handle nav bar gestures while Assistant is running over it");
-
-    public static final TogglableFlag ENABLE_HYBRID_HOTSEAT = new TogglableFlag(
-            "ENABLE_HYBRID_HOTSEAT", false, "Fill gaps in hotseat with predicted apps");
-
-    public static final TogglableFlag ENABLE_DEEP_SHORTCUT_ICON_CACHE = new TogglableFlag(
-            "ENABLE_DEEP_SHORTCUT_ICON_CACHE", true, "R/W deep shortcut in IconCache");
-
     public static void initialize(Context context) {
         // Avoid the disk read for user builds
         if (Utilities.IS_DEBUG_DEVICE) {
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 8823bde..cdc7061 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -57,6 +57,7 @@
 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;
@@ -120,7 +121,7 @@
     }
 
     public void recreateControllers() {
-        mControllers = mActivity.createTouchControllers();
+        mControllers = UiFactory.createTouchControllers(mActivity);
     }
 
     public ViewGroupFocusHelper getFocusIndicatorHelper() {
@@ -476,14 +477,14 @@
     public void onViewAdded(View child) {
         super.onViewAdded(child);
         updateChildIndices();
-        mActivity.onDragLayerHierarchyChanged();
+        UiFactory.onLauncherStateOrFocusChanged(mActivity);
     }
 
     @Override
     public void onViewRemoved(View child) {
         super.onViewRemoved(child);
         updateChildIndices();
-        mActivity.onDragLayerHierarchyChanged();
+        UiFactory.onLauncherStateOrFocusChanged(mActivity);
     }
 
     @Override
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 145885a..f66d07e 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -216,7 +216,8 @@
                 Object[] outObj = new Object[1];
                 int w = mBitmap.getWidth();
                 int h = mBitmap.getHeight();
-                Drawable dr = Utilities.getFullDrawable(mLauncher, info, w, h, outObj);
+                Drawable dr = Utilities.getFullDrawable(mLauncher, info, w, h,
+                        false /* flattenDrawable */, outObj);
 
                 if (dr instanceof AdaptiveIconDrawable) {
                     int blurMargin = (int) mLauncher.getResources()
diff --git a/src/com/android/launcher3/dragndrop/PinItemDragListener.java b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
index 869dd94..07eb0d6 100644
--- a/src/com/android/launcher3/dragndrop/PinItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
@@ -32,6 +32,7 @@
 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;
@@ -67,7 +68,7 @@
     public boolean init(Launcher launcher, boolean alreadyOnHome) {
         super.init(launcher, alreadyOnHome);
         if (!alreadyOnHome) {
-            launcher.useFadeOutAnimationForLauncherStart(mCancelSignal);
+            UiFactory.useFadeOutAnimationForLauncherStart(launcher, mCancelSignal);
         }
         return false;
     }
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 22dda41..65d593c 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -401,6 +401,7 @@
             mFolderName.setText("");
             mFolderName.setHint(R.string.folder_hint_text);
         }
+
         // In case any children didn't come across during loading, clean up the folder accordingly
         mFolderIcon.post(() -> {
             if (getItemCount() <= 1) {
@@ -409,22 +410,6 @@
         });
     }
 
-
-    /**
-     * Show suggested folder title.
-     */
-    public void showSuggestedTitle(CharSequence suggestName) {
-        if (FeatureFlags.FOLDER_NAME_SUGGEST.get() && mInfo.contents.size() == 2) {
-            if (!TextUtils.isEmpty(suggestName)) {
-                mFolderName.setHint(suggestName);
-                mFolderName.setText(suggestName);
-                mFolderName.showKeyboard();
-                mInfo.title = suggestName;
-            }
-            animateOpen();
-        }
-    }
-
     /**
      * Creates a new UserFolder, inflated from R.layout.user_folder.
      *
@@ -547,6 +532,8 @@
         // dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice.
         mDeleteFolderOnDropCompleted = false;
 
+        centerAboutIcon();
+
         AnimatorSet anim = new FolderAnimationManager(this, true /* isOpening */).getAnimator();
         anim.addListener(new AnimatorListenerAdapter() {
             @Override
@@ -605,6 +592,7 @@
         if (mDragController.isDragging()) {
             mDragController.forceTouchMove();
         }
+
         mContent.verifyVisibleHighResIcons(mContent.getNextPage());
     }
 
@@ -889,6 +877,7 @@
         // Reordering may have occured, and we need to save the new item locations. We do this once
         // at the end to prevent unnecessary database operations.
         updateItemLocationsInDatabaseBatch();
+
         // Use the item count to check for multi-page as the folder UI may not have
         // been refreshed yet.
         if (getItemCount() <= mContent.itemsPerPage()) {
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index fd6d1e3..3840639 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -58,14 +58,12 @@
 import com.android.launcher3.Workspace;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dot.FolderDotInfo;
 import com.android.launcher3.dragndrop.BaseItemDragListener;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.touch.ItemClickHandler;
-import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.views.IconLabelDotView;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
@@ -370,17 +368,12 @@
 
             if (!itemAdded) mPreviewItemManager.hidePreviewItem(index, true);
             final int finalIndex = index;
-
-            String[] suggestedNameOut = new String[1];
-            if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
-                Executors.UI_HELPER_EXECUTOR.post(() -> mLauncher.getFolderNameProvider()
-                        .getSuggestedFolderName(getContext(), mInfo.contents, suggestedNameOut));
-            }
-            postDelayed(() -> {
-                mPreviewItemManager.hidePreviewItem(finalIndex, false);
-                mFolder.showItem(item);
-                invalidate();
-                mFolder.showSuggestedTitle(suggestedNameOut[0]);
+            postDelayed(new Runnable() {
+                public void run() {
+                    mPreviewItemManager.hidePreviewItem(finalIndex, false);
+                    mFolder.showItem(item);
+                    invalidate();
+                }
             }, DROP_IN_ANIMATION_DURATION);
         } else {
             addItem(item);
diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java
deleted file mode 100644
index 0a1221e..0000000
--- a/src/com/android/launcher3/folder/FolderNameProvider.java
+++ /dev/null
@@ -1,61 +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.folder;
-
-import android.content.ComponentName;
-import android.content.Context;
-
-import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.WorkspaceItemInfo;
-
-import java.util.ArrayList;
-
-/**
- * Locates provider for the folder name.
- */
-public class FolderNameProvider {
-
-    /**
-     * Returns suggested folder name.
-     */
-    public CharSequence getSuggestedFolderName(Context context,
-            ArrayList<WorkspaceItemInfo> workspaceItemInfos, CharSequence[] suggestName) {
-        // Currently only run the algorithm on initial folder creation.
-        // For more than 2 items in the folder, the ranking algorithm for finding
-        // candidate folder name should be rewritten.
-        if (workspaceItemInfos.size() == 2) {
-            ComponentName cmp1 = workspaceItemInfos.get(0).getTargetComponent();
-            ComponentName cmp2 = workspaceItemInfos.get(1).getTargetComponent();
-
-            String pkgName0 = cmp1 == null ? "" : cmp1.getPackageName();
-            String pkgName1 = cmp2 == null ? "" : cmp2.getPackageName();
-            // If the two icons are from the same package,
-            // then assign the main icon's name
-            if (pkgName0.equals(pkgName1)) {
-                WorkspaceItemInfo wInfo0 = workspaceItemInfos.get(0);
-                WorkspaceItemInfo wInfo1 = workspaceItemInfos.get(1);
-                if (workspaceItemInfos.get(0).itemType == Favorites.ITEM_TYPE_APPLICATION) {
-                    suggestName[0] = wInfo0.title;
-                } else if (wInfo1.itemType == Favorites.ITEM_TYPE_APPLICATION) {
-                    suggestName[0] = wInfo1.title;
-                }
-                return suggestName[0];
-                // two icons are all shortcuts. Don't assign title
-            }
-        }
-        return suggestName[0];
-    }
-}
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 3b5fd59..54b363e 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -277,7 +277,6 @@
             page.removeAllViews();
             pages.add(page);
         }
-        mOrganizer.setFolderInfo(mFolder.getInfo());
         setupContentDimensions(itemCount);
 
         Iterator<CellLayout> pageItr = pages.iterator();
@@ -286,6 +285,7 @@
         int position = 0;
         int rank = 0;
 
+        mOrganizer.setFolderInfo(mFolder.getInfo());
         for (int i = 0; i < itemCount; i++) {
             View v = list.size() > i ? list.get(i) : null;
             if (currentPage == null || position >= mOrganizer.getMaxItemsPerPage()) {
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 5b3a05e..2d817e6 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -16,12 +16,10 @@
 
 package com.android.launcher3.folder;
 
-import static com.android.launcher3.FastBitmapDrawable.newIcon;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ENTER_INDEX;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.EXIT_INDEX;
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
 import static com.android.launcher3.folder.FolderIcon.DROP_IN_ANIMATION_DURATION;
-import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -40,6 +38,7 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.graphics.PreloadIconDrawable;
 
 import java.util.ArrayList;
@@ -67,6 +66,7 @@
 
     private final Context mContext;
     private final FolderIcon mIcon;
+    private final DrawableFactory mDrawableFactory;
     private final int mIconSize;
 
     // These variables are all associated with the drawing of the preview; they are stored
@@ -94,6 +94,7 @@
     public PreviewItemManager(FolderIcon icon) {
         mContext = icon.getContext();
         mIcon = icon;
+        mDrawableFactory = DrawableFactory.INSTANCE.get(mContext);
         mIconSize = Launcher.getLauncher(mContext).getDeviceProfile().folderChildIconSizePx;
     }
 
@@ -394,11 +395,11 @@
 
     private void setDrawable(PreviewItemDrawingParams p, WorkspaceItemInfo item) {
         if (item.hasPromiseIconUi()) {
-            PreloadIconDrawable drawable = newPendingIcon(mContext, item);
+            PreloadIconDrawable drawable = mDrawableFactory.newPendingIcon(mContext, item);
             drawable.setLevel(item.getInstallProgress());
             p.drawable = drawable;
         } else {
-            p.drawable = newIcon(mContext, item);
+            p.drawable = mDrawableFactory.newIcon(mContext, item);
         }
         p.drawable.setBounds(0, 0, mIconSize, mIconSize);
         p.item = item;
diff --git a/src/com/android/launcher3/graphics/DrawableFactory.java b/src/com/android/launcher3/graphics/DrawableFactory.java
new file mode 100644
index 0000000..837301f
--- /dev/null
+++ b/src/com/android/launcher3/graphics/DrawableFactory.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.graphics;
+
+import static com.android.launcher3.graphics.IconShape.getShapePath;
+import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.FastBitmapDrawable;
+import com.android.launcher3.ItemInfoWithIcon;
+import com.android.launcher3.R;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.ResourceBasedOverride;
+
+/**
+ * Factory for creating new drawables.
+ */
+public class DrawableFactory implements ResourceBasedOverride {
+
+    public static final MainThreadInitializedObject<DrawableFactory> INSTANCE =
+            forOverride(DrawableFactory.class, R.string.drawable_factory_class);
+
+    protected final UserHandle mMyUser = Process.myUserHandle();
+    protected final ArrayMap<UserHandle, Bitmap> mUserBadges = new ArrayMap<>();
+
+    /**
+     * Returns a FastBitmapDrawable with the icon.
+     */
+    public FastBitmapDrawable newIcon(Context context, ItemInfoWithIcon info) {
+        FastBitmapDrawable drawable = info.usingLowResIcon()
+                ? new PlaceHolderIconDrawable(info, getShapePath(), context)
+                : new FastBitmapDrawable(info);
+        drawable.setIsDisabled(info.isDisabled());
+        return drawable;
+    }
+
+    public FastBitmapDrawable newIcon(Context context, BitmapInfo info, ActivityInfo target) {
+        return info.isLowRes()
+                ? new PlaceHolderIconDrawable(info, getShapePath(), context)
+                : new FastBitmapDrawable(info);
+    }
+
+    /**
+     * Returns a FastBitmapDrawable with the icon.
+     */
+    public PreloadIconDrawable newPendingIcon(Context context, ItemInfoWithIcon info) {
+        return new PreloadIconDrawable(info, getShapePath(), context);
+    }
+
+    /**
+     * Returns a drawable that can be used as a badge for the user or null.
+     */
+    @UiThread
+    public Drawable getBadgeForUser(UserHandle user, Context context, int badgeSize) {
+        if (mMyUser.equals(user)) {
+            return null;
+        }
+
+        Bitmap badgeBitmap = getUserBadge(user, context, badgeSize);
+        FastBitmapDrawable d = new FastBitmapDrawable(badgeBitmap);
+        d.setFilterBitmap(true);
+        d.setBounds(0, 0, badgeBitmap.getWidth(), badgeBitmap.getHeight());
+        return d;
+    }
+
+    protected synchronized Bitmap getUserBadge(UserHandle user, Context context, int badgeSize) {
+        Bitmap badgeBitmap = mUserBadges.get(user);
+        if (badgeBitmap != null) {
+            return badgeBitmap;
+        }
+
+        final Resources res = context.getApplicationContext().getResources();
+        badgeBitmap = Bitmap.createBitmap(badgeSize, badgeSize, Bitmap.Config.ARGB_8888);
+
+        Drawable drawable = context.getPackageManager().getUserBadgedDrawableForDensity(
+                new BitmapDrawable(res, badgeBitmap), user, new Rect(0, 0, badgeSize, badgeSize),
+                0);
+        if (drawable instanceof BitmapDrawable) {
+            badgeBitmap = ((BitmapDrawable) drawable).getBitmap();
+        } else {
+            badgeBitmap.eraseColor(Color.TRANSPARENT);
+            Canvas c = new Canvas(badgeBitmap);
+            drawable.setBounds(0, 0, badgeSize, badgeSize);
+            drawable.draw(c);
+            c.setBitmap(null);
+        }
+
+        mUserBadges.put(user, badgeBitmap);
+        return badgeBitmap;
+    }
+}
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 2badb6e..d7b845b 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -50,8 +50,8 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.WorkspaceLayoutManager;
 import com.android.launcher3.allapps.SearchUiManager;
 import com.android.launcher3.config.FeatureFlags;
@@ -105,7 +105,7 @@
                 Build.VERSION.SDK_INT);
 
         mWorkspaceItemInfo = new WorkspaceItemInfo();
-        mWorkspaceItemInfo.bitmap = iconInfo;
+        mWorkspaceItemInfo.applyFrom(iconInfo);
         mWorkspaceItemInfo.intent = new Intent();
         mWorkspaceItemInfo.contentDescription = mWorkspaceItemInfo.title =
                 context.getString(R.string.label_application);
diff --git a/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java b/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
index d347e8f..23745cb 100644
--- a/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
@@ -17,14 +17,14 @@
 
 import static androidx.core.graphics.ColorUtils.compositeColors;
 
-import static com.android.launcher3.graphics.IconShape.getShapePath;
-
 import android.content.Context;
+import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Path;
 import android.graphics.Rect;
 
 import com.android.launcher3.FastBitmapDrawable;
+import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.util.Themes;
@@ -37,12 +37,20 @@
     // Path in [0, 100] bounds.
     private final Path mProgressPath;
 
-    public PlaceHolderIconDrawable(BitmapInfo info, Context context) {
-        super(info);
+    public PlaceHolderIconDrawable(BitmapInfo info, Path progressPath, Context context) {
+        this(info.icon, info.color, progressPath, context);
+    }
 
-        mProgressPath = getShapePath();
+    public PlaceHolderIconDrawable(ItemInfoWithIcon info, Path progressPath, Context context) {
+        this(info.iconBitmap, info.iconColor, progressPath, context);
+    }
+
+    protected PlaceHolderIconDrawable(Bitmap b, int iconColor, Path progressPath, Context context) {
+        super(b, iconColor);
+
+        mProgressPath = progressPath;
         mPaint.setColor(compositeColors(
-                Themes.getAttrColor(context, R.attr.loadingIconColor), info.color));
+                Themes.getAttrColor(context, R.attr.loadingIconColor), iconColor));
     }
 
     @Override
diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
index b0e1db1..cc4c2ef 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -18,7 +18,6 @@
 package com.android.launcher3.graphics;
 
 import static com.android.launcher3.graphics.IconShape.DEFAULT_PATH_SIZE;
-import static com.android.launcher3.graphics.IconShape.getShapePath;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -102,10 +101,13 @@
 
     private ObjectAnimator mCurrentAnim;
 
-    public PreloadIconDrawable(ItemInfoWithIcon info, Context context) {
-        super(info.bitmap);
+    /**
+     * @param progressPath fixed path in the bounds [0, 0, 100, 100] representing a progress bar.
+     */
+    public PreloadIconDrawable(ItemInfoWithIcon info, Path progressPath, Context context) {
+        super(info);
         mItem = info;
-        mProgressPath = getShapePath();
+        mProgressPath = progressPath;
         mScaledTrackPath = new Path();
         mScaledProgressPath = new Path();
 
@@ -287,11 +289,4 @@
         }
         invalidateSelf();
     }
-
-    /**
-     * Returns a FastBitmapDrawable with the icon.
-     */
-    public static PreloadIconDrawable newPendingIcon(Context context, ItemInfoWithIcon info) {
-        return new PreloadIconDrawable(info, context);
-    }
 }
diff --git a/src/com/android/launcher3/icons/ClockDrawableWrapper.java b/src/com/android/launcher3/icons/ClockDrawableWrapper.java
deleted file mode 100644
index b7dd092..0000000
--- a/src/com/android/launcher3/icons/ClockDrawableWrapper.java
+++ /dev/null
@@ -1,328 +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.icons;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Process;
-import android.os.SystemClock;
-import android.util.Log;
-
-import com.android.launcher3.FastBitmapDrawable;
-
-import java.util.Calendar;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Wrapper over {@link AdaptiveIconDrawable} to intercept icon flattening logic for dynamic
- * clock icons
- */
-@TargetApi(Build.VERSION_CODES.O)
-public class ClockDrawableWrapper extends AdaptiveIconDrawable implements BitmapInfo.Extender {
-
-    private static final String TAG = "ClockDrawableWrapper";
-
-    private static final boolean DISABLE_SECONDS = true;
-
-    // Time after which the clock icon should check for an update. The actual invalidate
-    // will only happen in case of any change.
-    public static final long TICK_MS = DISABLE_SECONDS ? TimeUnit.MINUTES.toMillis(1) : 200L;
-
-    private static final String LAUNCHER_PACKAGE = "com.android.launcher3";
-    private static final String ROUND_ICON_METADATA_KEY = LAUNCHER_PACKAGE
-            + ".LEVEL_PER_TICK_ICON_ROUND";
-    private static final String HOUR_INDEX_METADATA_KEY = LAUNCHER_PACKAGE + ".HOUR_LAYER_INDEX";
-    private static final String MINUTE_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
-            + ".MINUTE_LAYER_INDEX";
-    private static final String SECOND_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
-            + ".SECOND_LAYER_INDEX";
-    private static final String DEFAULT_HOUR_METADATA_KEY = LAUNCHER_PACKAGE
-            + ".DEFAULT_HOUR";
-    private static final String DEFAULT_MINUTE_METADATA_KEY = LAUNCHER_PACKAGE
-            + ".DEFAULT_MINUTE";
-    private static final String DEFAULT_SECOND_METADATA_KEY = LAUNCHER_PACKAGE
-            + ".DEFAULT_SECOND";
-
-    /* Number of levels to jump per second for the second hand */
-    private static final int LEVELS_PER_SECOND = 10;
-
-    public static final int INVALID_VALUE = -1;
-
-    private final AnimationInfo mAnimationInfo = new AnimationInfo();
-    private int mTargetSdkVersion;
-
-    public ClockDrawableWrapper(AdaptiveIconDrawable base) {
-        super(base.getBackground(), base.getForeground());
-    }
-
-    /**
-     * Loads and returns the wrapper from the provided package, or returns null
-     * if it is unable to load.
-     */
-    public static ClockDrawableWrapper forPackage(Context context, String pkg, int iconDpi) {
-        try {
-            PackageManager pm = context.getPackageManager();
-            ApplicationInfo appInfo =  pm.getApplicationInfo(pkg,
-                    PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA);
-            final Bundle metadata = appInfo.metaData;
-            if (metadata == null) {
-                return null;
-            }
-            int drawableId = metadata.getInt(ROUND_ICON_METADATA_KEY, 0);
-            if (drawableId == 0) {
-                return null;
-            }
-
-            Drawable drawable = pm.getResourcesForApplication(appInfo).getDrawableForDensity(
-                    drawableId, iconDpi).mutate();
-            if (!(drawable instanceof AdaptiveIconDrawable)) {
-                return null;
-            }
-
-            ClockDrawableWrapper wrapper =
-                    new ClockDrawableWrapper((AdaptiveIconDrawable) drawable);
-            wrapper.mTargetSdkVersion = appInfo.targetSdkVersion;
-            AnimationInfo info = wrapper.mAnimationInfo;
-
-            info.baseDrawableState = drawable.getConstantState();
-
-            info.hourLayerIndex = metadata.getInt(HOUR_INDEX_METADATA_KEY, INVALID_VALUE);
-            info.minuteLayerIndex = metadata.getInt(MINUTE_INDEX_METADATA_KEY, INVALID_VALUE);
-            info.secondLayerIndex = metadata.getInt(SECOND_INDEX_METADATA_KEY, INVALID_VALUE);
-
-            info.defaultHour = metadata.getInt(DEFAULT_HOUR_METADATA_KEY, 0);
-            info.defaultMinute = metadata.getInt(DEFAULT_MINUTE_METADATA_KEY, 0);
-            info.defaultSecond = metadata.getInt(DEFAULT_SECOND_METADATA_KEY, 0);
-
-            LayerDrawable foreground = (LayerDrawable) wrapper.getForeground();
-            int layerCount = foreground.getNumberOfLayers();
-            if (info.hourLayerIndex < 0 || info.hourLayerIndex >= layerCount) {
-                info.hourLayerIndex = INVALID_VALUE;
-            }
-            if (info.minuteLayerIndex < 0 || info.minuteLayerIndex >= layerCount) {
-                info.minuteLayerIndex = INVALID_VALUE;
-            }
-            if (info.secondLayerIndex < 0 || info.secondLayerIndex >= layerCount) {
-                info.secondLayerIndex = INVALID_VALUE;
-            } else if (DISABLE_SECONDS) {
-                foreground.setDrawable(info.secondLayerIndex, null);
-                info.secondLayerIndex = INVALID_VALUE;
-            }
-            return wrapper;
-        } catch (Exception e) {
-            Log.d(TAG, "Unable to load clock drawable info", e);
-        }
-        return null;
-    }
-
-    @Override
-    public BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory) {
-        iconFactory.disableColorExtraction();
-        float [] scale = new float[1];
-        AdaptiveIconDrawable background = new AdaptiveIconDrawable(
-                getBackground().getConstantState().newDrawable(), null);
-        BitmapInfo bitmapInfo = iconFactory.createBadgedIconBitmap(background,
-                Process.myUserHandle(), mTargetSdkVersion, false, scale);
-
-        return new ClockBitmapInfo(bitmap, color, scale[0], mAnimationInfo, bitmapInfo.icon);
-    }
-
-    @Override
-    public void prepareToDrawOnUi() {
-        mAnimationInfo.applyTime(Calendar.getInstance(), (LayerDrawable) getForeground());
-    }
-
-    private static class AnimationInfo {
-
-        public ConstantState baseDrawableState;
-
-        public int hourLayerIndex;
-        public int minuteLayerIndex;
-        public int secondLayerIndex;
-        public int defaultHour;
-        public int defaultMinute;
-        public int defaultSecond;
-
-        boolean applyTime(Calendar time, LayerDrawable foregroundDrawable) {
-            time.setTimeInMillis(System.currentTimeMillis());
-
-            // We need to rotate by the difference from the default time if one is specified.
-            int convertedHour = (time.get(Calendar.HOUR) + (12 - defaultHour)) % 12;
-            int convertedMinute = (time.get(Calendar.MINUTE) + (60 - defaultMinute)) % 60;
-            int convertedSecond = (time.get(Calendar.SECOND) + (60 - defaultSecond)) % 60;
-
-            boolean invalidate = false;
-            if (hourLayerIndex != INVALID_VALUE) {
-                final Drawable hour = foregroundDrawable.getDrawable(hourLayerIndex);
-                if (hour.setLevel(convertedHour * 60 + time.get(Calendar.MINUTE))) {
-                    invalidate = true;
-                }
-            }
-
-            if (minuteLayerIndex != INVALID_VALUE) {
-                final Drawable minute = foregroundDrawable.getDrawable(minuteLayerIndex);
-                if (minute.setLevel(time.get(Calendar.HOUR) * 60 + convertedMinute)) {
-                    invalidate = true;
-                }
-            }
-
-            if (secondLayerIndex != INVALID_VALUE) {
-                final Drawable second = foregroundDrawable.getDrawable(secondLayerIndex);
-                if (second.setLevel(convertedSecond * LEVELS_PER_SECOND)) {
-                    invalidate = true;
-                }
-            }
-
-            return invalidate;
-        }
-    }
-
-    private static class ClockBitmapInfo extends BitmapInfo implements FastBitmapDrawable.Factory {
-
-        public final float scale;
-        public final int offset;
-        public final AnimationInfo animInfo;
-        public final Bitmap mFlattenedBackground;
-
-        ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo,
-                Bitmap background) {
-            super(icon, color);
-            this.scale = scale;
-            this.animInfo = animInfo;
-            this.offset = (int) Math.ceil(ShadowGenerator.BLUR_FACTOR * icon.getWidth());
-            this.mFlattenedBackground = background;
-        }
-
-        @Override
-        public FastBitmapDrawable newDrawable() {
-            return new ClockIconDrawable(this);
-        }
-    }
-
-    private static class ClockIconDrawable extends FastBitmapDrawable implements Runnable {
-
-        private final Calendar mTime = Calendar.getInstance();
-
-        private final ClockBitmapInfo mInfo;
-
-        private final AdaptiveIconDrawable mFullDrawable;
-        private final LayerDrawable mForeground;
-
-        ClockIconDrawable(ClockBitmapInfo clockInfo) {
-            super(clockInfo);
-
-            mInfo = clockInfo;
-
-            mFullDrawable = (AdaptiveIconDrawable) mInfo.animInfo.baseDrawableState.newDrawable();
-            mForeground = (LayerDrawable) mFullDrawable.getForeground();
-        }
-
-        @Override
-        protected void onBoundsChange(Rect bounds) {
-            super.onBoundsChange(bounds);
-            mFullDrawable.setBounds(bounds);
-        }
-
-        @Override
-        public void drawInternal(Canvas canvas, Rect bounds) {
-            if (mInfo == null) {
-                super.drawInternal(canvas, bounds);
-                return;
-            }
-            // draw the background that is already flattened to a bitmap
-            canvas.drawBitmap(mInfo.mFlattenedBackground, null, bounds, mPaint);
-
-            // prepare and draw the foreground
-            mInfo.animInfo.applyTime(mTime, mForeground);
-
-            canvas.scale(mInfo.scale, mInfo.scale,
-                    bounds.exactCenterX() + mInfo.offset, bounds.exactCenterY() + mInfo.offset);
-            canvas.clipPath(mFullDrawable.getIconMask());
-            mForeground.draw(canvas);
-
-            reschedule();
-        }
-
-        @Override
-        protected void updateFilter() {
-            super.updateFilter();
-            mFullDrawable.setColorFilter(mPaint.getColorFilter());
-        }
-
-        @Override
-        public void run() {
-            if (mInfo.animInfo.applyTime(mTime, mForeground)) {
-                invalidateSelf();
-            } else {
-                reschedule();
-            }
-        }
-
-        @Override
-        public boolean setVisible(boolean visible, boolean restart) {
-            boolean result = super.setVisible(visible, restart);
-            if (visible) {
-                reschedule();
-            } else {
-                unscheduleSelf(this);
-            }
-            return result;
-        }
-
-        private void reschedule() {
-            if (!isVisible()) {
-                return;
-            }
-
-            unscheduleSelf(this);
-            final long upTime = SystemClock.uptimeMillis();
-            final long step = TICK_MS; /* tick every 200 ms */
-            scheduleSelf(this, upTime - ((upTime % step)) + step);
-        }
-
-        @Override
-        public ConstantState getConstantState() {
-            return new ClockConstantState(mInfo, isDisabled());
-        }
-
-        private static class ClockConstantState extends MyConstantState {
-
-            private final ClockBitmapInfo mInfo;
-
-            ClockConstantState(ClockBitmapInfo info, boolean isDisabled) {
-                super(info.icon, info.color, isDisabled);
-                mInfo = info;
-            }
-
-            @Override
-            public FastBitmapDrawable newDrawable() {
-                ClockIconDrawable drawable = new ClockIconDrawable(mInfo);
-                drawable.setIsDisabled(mIsDisabled);
-                return drawable;
-            }
-        }
-    }
-}
diff --git a/src/com/android/launcher3/icons/ComponentWithLabel.java b/src/com/android/launcher3/icons/ComponentWithLabel.java
index f7ee5f9..832956d 100644
--- a/src/com/android/launcher3/icons/ComponentWithLabel.java
+++ b/src/com/android/launcher3/icons/ComponentWithLabel.java
@@ -57,8 +57,10 @@
         }
 
         @Override
-        public BitmapInfo loadIcon(Context context, ComponentWithLabel object) {
-            return BitmapInfo.LOW_RES_INFO;
+        public void loadIcon(Context context,
+                ComponentWithLabel object, BitmapInfo target) {
+            // Do not load icon.
+            target.icon = BitmapInfo.LOW_RES_ICON;
         }
 
         @Override
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index 4ac6ff4..ad01f9f 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -38,6 +38,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.AppInfo;
+import com.android.launcher3.IconProvider;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.LauncherFiles;
@@ -51,7 +52,6 @@
 import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.InstantAppResolver;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
@@ -85,7 +85,7 @@
         mLauncherApps = mContext.getSystemService(LauncherApps.class);
         mUserManager = UserManagerCompat.getInstance(mContext);
         mInstantAppResolver = InstantAppResolver.newInstance(mContext);
-        mIconProvider = new IconProvider(context);
+        mIconProvider = IconProvider.INSTANCE.get(context);
     }
 
     @Override
@@ -165,7 +165,7 @@
         CacheEntry entry = cacheLocked(application.componentName,
                 application.user, () -> null, mLauncherActivityInfoCachingLogic,
                 false, application.usingLowResIcon());
-        if (entry.bitmap != null && !isDefaultIcon(entry.bitmap, application.user)) {
+        if (entry.icon != null && !isDefaultIcon(entry.icon, application.user)) {
             applyCacheEntry(entry, application);
         }
     }
@@ -195,7 +195,7 @@
         // null info means not installed, but if we have a component from the intent then
         // we should still look in the cache for restored app icons.
         if (info.getTargetComponent() == null) {
-            info.bitmap = getDefaultIcon(info.user);
+            info.applyFrom(getDefaultIcon(info.user));
             info.title = "";
             info.contentDescription = "";
         } else {
@@ -238,11 +238,15 @@
     protected void applyCacheEntry(CacheEntry entry, ItemInfoWithIcon info) {
         info.title = Utilities.trim(entry.title);
         info.contentDescription = entry.contentDescription;
-        info.bitmap = (entry.bitmap == null) ? getDefaultIcon(info.user) : entry.bitmap;
+        info.applyFrom((entry.icon == null) ? getDefaultIcon(info.user) : entry);
     }
 
     public Drawable getFullResIcon(LauncherActivityInfo info) {
-        return mIconProvider.getIcon(info, mIconDpi);
+        return getFullResIcon(info, true);
+    }
+
+    public Drawable getFullResIcon(LauncherActivityInfo info, boolean flattenDrawable) {
+        return mIconProvider.getIcon(info, mIconDpi, flattenDrawable);
     }
 
     public void updateSessionCache(PackageUserKey key, PackageInstaller.SessionInfo info) {
@@ -255,15 +259,6 @@
                 + ",flags_asi:" + FeatureFlags.APP_SEARCH_IMPROVEMENTS.get();
     }
 
-    @Override
-    protected boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
-        if (mIconProvider.isClockIcon(cacheKey)) {
-            // For clock icon, we always load the dynamic icon
-            return false;
-        }
-        return super.getEntryFromDB(cacheKey, entry, lowRes);
-    }
-
     public static abstract class IconLoadRequest extends HandlerRunnable {
         IconLoadRequest(Handler handler, Runnable endRunnable) {
             super(handler, endRunnable);
diff --git a/src/com/android/launcher3/icons/IconProvider.java b/src/com/android/launcher3/icons/IconProvider.java
deleted file mode 100644
index 26b7eae..0000000
--- a/src/com/android/launcher3/icons/IconProvider.java
+++ /dev/null
@@ -1,251 +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.icons;
-
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ActivityInfo;
-import android.content.pm.LauncherActivityInfo;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Process;
-import android.os.UserHandle;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.launcher3.R;
-import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.icons.BitmapInfo.Extender;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.SafeCloseable;
-
-import java.util.Calendar;
-import java.util.function.BiConsumer;
-import java.util.function.BiFunction;
-
-/**
- * Class to handle icon loading from different packages
- */
-public class IconProvider {
-
-    private static final String TAG = "IconProvider";
-    private static final boolean DEBUG = false;
-
-    private static final String ICON_METADATA_KEY_PREFIX = ".dynamic_icons";
-
-    private static final String SYSTEM_STATE_SEPARATOR = " ";
-
-    // Default value returned if there are problems getting resources.
-    private static final int NO_ID = 0;
-
-    private static final BiFunction<LauncherActivityInfo, Integer, Drawable> LAI_LOADER =
-            LauncherActivityInfo::getIcon;
-
-    private static final BiFunction<ActivityInfo, PackageManager, Drawable> AI_LOADER =
-            ActivityInfo::loadUnbadgedIcon;
-
-
-    private final Context mContext;
-    private final ComponentName mCalendar;
-    private final ComponentName mClock;
-
-    public IconProvider(Context context) {
-        mContext = context;
-        mCalendar = parseComponentOrNull(context, R.string.calendar_component_name);
-        mClock = parseComponentOrNull(context, R.string.clock_component_name);
-    }
-
-    /**
-     * Adds any modification to the provided systemState for dynamic icons. This system state
-     * is used by caches to check for icon invalidation.
-     */
-    public String getSystemStateForPackage(String systemState, String packageName) {
-        if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
-            return systemState + SYSTEM_STATE_SEPARATOR + getDay();
-        } else {
-            return systemState;
-        }
-    }
-
-    /**
-     * Loads the icon for the provided LauncherActivityInfo such that it can be drawn directly
-     * on the UI
-     */
-    public Drawable getIconForUI(LauncherActivityInfo info, int iconDpi) {
-        Drawable icon = getIcon(info, iconDpi);
-        if (icon instanceof BitmapInfo.Extender) {
-            ((Extender) icon).prepareToDrawOnUi();
-        }
-        return icon;
-    }
-
-    /**
-     * Loads the icon for the provided LauncherActivityInfo
-     */
-    public Drawable getIcon(LauncherActivityInfo info, int iconDpi) {
-        return getIcon(info.getApplicationInfo().packageName, info.getUser(),
-                info, iconDpi, LAI_LOADER);
-    }
-
-    /**
-     * Loads the icon for the provided activity info
-     */
-    public Drawable getIcon(ActivityInfo info, UserHandle user) {
-        return getIcon(info.applicationInfo.packageName, user, info, mContext.getPackageManager(),
-                AI_LOADER);
-    }
-
-    private <T, P> Drawable getIcon(String packageName, UserHandle user, T obj, P param,
-            BiFunction<T, P, Drawable> loader) {
-        Drawable icon = null;
-        if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
-            icon = loadCalendarDrawable(0);
-        } else if (mClock != null
-                && mClock.getPackageName().equals(packageName)
-                && Process.myUserHandle().equals(user)) {
-            icon = loadClockDrawable(0);
-        }
-        return icon == null ? loader.apply(obj, param) : icon;
-    }
-
-    private Drawable loadCalendarDrawable(int iconDpi) {
-        PackageManager pm = mContext.getPackageManager();
-        try {
-            final Bundle metadata = pm.getActivityInfo(
-                    mCalendar,
-                    PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA)
-                    .metaData;
-            final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName());
-            final int id = getDynamicIconId(metadata, resources);
-            if (id != NO_ID) {
-                if (DEBUG) Log.d(TAG, "Got icon #" + id);
-                return resources.getDrawableForDensity(id, iconDpi, null /* theme */);
-            }
-        } catch (PackageManager.NameNotFoundException e) {
-            if (DEBUG) {
-                Log.d(TAG, "Could not get activityinfo or resources for package: "
-                        + mCalendar.getPackageName());
-            }
-        }
-        return null;
-    }
-
-    private Drawable loadClockDrawable(int iconDpi) {
-        return ClockDrawableWrapper.forPackage(mContext, mClock.getPackageName(), iconDpi);
-    }
-
-    protected boolean isClockIcon(ComponentKey key) {
-        return mClock != null && mClock.equals(key.componentName)
-                && Process.myUserHandle().equals(key.user);
-    }
-
-    /**
-     * @param metadata metadata of the default activity of Calendar
-     * @param resources from the Calendar package
-     * @return the resource id for today's Calendar icon; 0 if resources cannot be found.
-     */
-    private int getDynamicIconId(Bundle metadata, Resources resources) {
-        if (metadata == null) {
-            return NO_ID;
-        }
-        String key = mCalendar.getPackageName() + ICON_METADATA_KEY_PREFIX;
-        final int arrayId = metadata.getInt(key, NO_ID);
-        if (arrayId == NO_ID) {
-            return NO_ID;
-        }
-        try {
-            return resources.obtainTypedArray(arrayId).getResourceId(getDay(), NO_ID);
-        } catch (Resources.NotFoundException e) {
-            if (DEBUG) {
-                Log.d(TAG, "package defines '" + key + "' but corresponding array not found");
-            }
-            return NO_ID;
-        }
-    }
-
-    /**
-     * @return Today's day of the month, zero-indexed.
-     */
-    private int getDay() {
-        return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1;
-    }
-
-
-    /**
-     * Registers a callback to listen for calendar icon changes.
-     * The callback receives the packageName for the calendar icon
-     */
-    public static SafeCloseable registerIconChangeListener(Context context,
-            BiConsumer<String, UserHandle> callback, Handler handler) {
-        ComponentName calendar = parseComponentOrNull(context, R.string.calendar_component_name);
-        ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name);
-
-        if (calendar == null && clock == null) {
-            return () -> { };
-        }
-
-        BroadcastReceiver receiver = new DateTimeChangeReceiver(callback);
-        final IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
-        if (calendar != null) {
-            filter.addAction(Intent.ACTION_TIME_CHANGED);
-            filter.addAction(Intent.ACTION_DATE_CHANGED);
-        }
-        context.registerReceiver(receiver, filter, null, handler);
-
-        return () -> context.unregisterReceiver(receiver);
-    }
-
-    private static class DateTimeChangeReceiver extends BroadcastReceiver {
-
-        private final BiConsumer<String, UserHandle> mCallback;
-
-        DateTimeChangeReceiver(BiConsumer<String, UserHandle> callback) {
-            mCallback = callback;
-        }
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
-                ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name);
-                if (clock != null) {
-                    mCallback.accept(clock.getPackageName(), Process.myUserHandle());
-                }
-            }
-
-            ComponentName calendar =
-                    parseComponentOrNull(context, R.string.calendar_component_name);
-            if (calendar != null) {
-                for (UserHandle user : UserManagerCompat.getInstance(context).getUserProfiles()) {
-                    mCallback.accept(calendar.getPackageName(), user);
-                }
-            }
-
-        }
-    }
-
-    private static ComponentName parseComponentOrNull(Context context, int resId) {
-        String cn = context.getString(resId);
-        return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn);
-
-    }
-}
diff --git a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java b/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
index 93de35a..f9a94da 100644
--- a/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
+++ b/src/com/android/launcher3/icons/LauncherActivityCachingLogic.java
@@ -20,6 +20,7 @@
 import android.content.pm.LauncherActivityInfo;
 import android.os.UserHandle;
 
+import com.android.launcher3.IconProvider;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.cache.CachingLogic;
 import com.android.launcher3.util.ResourceBasedOverride;
@@ -54,11 +55,13 @@
     }
 
     @Override
-    public BitmapInfo loadIcon(Context context, LauncherActivityInfo object) {
-        try (LauncherIcons li = LauncherIcons.obtain(context)) {
-            return li.createBadgedIconBitmap(new IconProvider(context)
-                            .getIcon(object, li.mFillResIconDpi),
-                    object.getUser(), object.getApplicationInfo().targetSdkVersion);
-        }
+    public void loadIcon(Context context, LauncherActivityInfo object,
+            BitmapInfo target) {
+        LauncherIcons li = LauncherIcons.obtain(context);
+        li.createBadgedIconBitmap(
+                IconProvider.INSTANCE.get(context)
+                        .getIcon(object, li.mFillResIconDpi, true /* flattenDrawable */),
+                object.getUser(), object.getApplicationInfo().targetSdkVersion).applyTo(target);
+        li.recycle();
     }
 }
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index 02e2d70..44c67e9 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -32,7 +32,7 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.R;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
@@ -46,6 +46,8 @@
  */
 public class LauncherIcons extends BaseIconFactory implements AutoCloseable {
 
+    private static final String EXTRA_BADGEPKG = "badge_package";
+
     private static final Object sPoolSync = new Object();
     private static LauncherIcons sPool;
     private static int sPoolId = 0;
@@ -114,6 +116,7 @@
     }
 
     // below methods should also migrate to BaseIconFactory
+    @WorkerThread
     public BitmapInfo createShortcutIcon(ShortcutInfo shortcutInfo) {
         return createShortcutIcon(shortcutInfo, true /* badged */);
     }
@@ -123,58 +126,24 @@
         return createShortcutIcon(shortcutInfo, badged, null);
     }
 
+    @WorkerThread
     public BitmapInfo createShortcutIcon(ShortcutInfo shortcutInfo, boolean badged,
             @Nullable Supplier<ItemInfoWithIcon> fallbackIconProvider) {
-        if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
-            return createShortcutIconCached(shortcutInfo, badged, true, fallbackIconProvider);
-        } else {
-            return createShortcutIconLegacy(shortcutInfo, badged, fallbackIconProvider);
-        }
-    }
-
-    public BitmapInfo createShortcutIconLegacy(ShortcutInfo shortcutInfo, boolean badged,
-            @Nullable Supplier<ItemInfoWithIcon> fallbackIconProvider) {
-        Drawable unbadgedDrawable = DeepShortcutManager.getInstance(mContext)
-                .getShortcutIconDrawable(shortcutInfo, mFillResIconDpi);
-        IconCache cache = LauncherAppState.getInstance(mContext).getIconCache();
-        final Bitmap unbadgedBitmap;
-        if (bitmapInfo.icon != null) {
-            unbadgedBitmap = bitmapInfo.icon;
-        } else {
-            if (fallbackIconProvider != null) {
-                // Fallback icons are already badged and with appropriate shadow
-                ItemInfoWithIcon fullIcon = fallbackIconProvider.get();
-                if (fullIcon != null && fullIcon.bitmap != null) {
-                    return fullIcon.bitmap;
-                }
-            }
-            unbadgedBitmap = cache.getDefaultIcon(Process.myUserHandle()).icon;
-        }
-
-        if (!badged) {
-            return BitmapInfo.of(unbadgedBitmap, Themes.getColorAccent(mContext));
-        }
-
-        final Bitmap unbadgedfinal = unbadgedBitmap;
-        final ItemInfoWithIcon badge = getShortcutInfoBadge(shortcutInfo, cache);
-
-        Bitmap icon = BitmapRenderer.createHardwareBitmap(mIconBitmapSize, mIconBitmapSize, (c) -> {
-            getShadowGenerator().recreateIcon(unbadgedfinal, c);
-            badgeWithDrawable(c, new FastBitmapDrawable(badge.bitmap));
-        });
-        return BitmapInfo.of(icon, badge.bitmap.color);
+        return createShortcutIcon(shortcutInfo, badged, true, fallbackIconProvider);
     }
 
     @WorkerThread
-    public BitmapInfo createShortcutIconCached(ShortcutInfo shortcutInfo, boolean badged,
+    public BitmapInfo createShortcutIcon(ShortcutInfo shortcutInfo, boolean badged,
             boolean useCache, @Nullable Supplier<ItemInfoWithIcon> fallbackIconProvider) {
         IconCache cache = LauncherAppState.getInstance(mContext).getIconCache();
         final BitmapInfo bitmapInfo;
         if (useCache) {
-            bitmapInfo = cache.getDeepShortcutTitleAndIcon(shortcutInfo).bitmap;
+            bitmapInfo = cache.getDeepShortcutTitleAndIcon(shortcutInfo);
         } else {
-            bitmapInfo = new ShortcutCachingLogic().loadIcon(mContext, shortcutInfo);
+            bitmapInfo = new BitmapInfo();
+            new ShortcutCachingLogic().loadIcon(mContext, shortcutInfo, bitmapInfo);
         }
+
         final Bitmap unbadgedBitmap;
         if (bitmapInfo.icon != null) {
             unbadgedBitmap = bitmapInfo.icon;
@@ -182,30 +151,37 @@
             if (fallbackIconProvider != null) {
                 // Fallback icons are already badged and with appropriate shadow
                 ItemInfoWithIcon fullIcon = fallbackIconProvider.get();
-                if (fullIcon != null && fullIcon.bitmap != null) {
-                    return fullIcon.bitmap;
+                if (fullIcon != null && fullIcon.iconBitmap != null) {
+                    BitmapInfo result = new BitmapInfo();
+                    result.icon = fullIcon.iconBitmap;
+                    result.color = fullIcon.iconColor;
+                    return result;
                 }
             }
             unbadgedBitmap = cache.getDefaultIcon(Process.myUserHandle()).icon;
         }
 
+        BitmapInfo result = new BitmapInfo();
         if (!badged) {
-            return BitmapInfo.of(unbadgedBitmap, Themes.getColorAccent(mContext));
+            result.color = Themes.getColorAccent(mContext);
+            result.icon = unbadgedBitmap;
+            return result;
         }
 
         final Bitmap unbadgedfinal = unbadgedBitmap;
         final ItemInfoWithIcon badge = getShortcutInfoBadge(shortcutInfo, cache);
 
-        Bitmap icon = BitmapRenderer.createHardwareBitmap(mIconBitmapSize, mIconBitmapSize, (c) -> {
+        result.color = badge.iconColor;
+        result.icon = BitmapRenderer.createHardwareBitmap(mIconBitmapSize, mIconBitmapSize, (c) -> {
             getShadowGenerator().recreateIcon(unbadgedfinal, c);
-            badgeWithDrawable(c, new FastBitmapDrawable(badge.bitmap));
+            badgeWithDrawable(c, new FastBitmapDrawable(badge));
         });
-        return BitmapInfo.of(icon, badge.bitmap.color);
+        return result;
     }
 
     public ItemInfoWithIcon getShortcutInfoBadge(ShortcutInfo shortcutInfo, IconCache cache) {
         ComponentName cn = shortcutInfo.getActivity();
-        String badgePkg = shortcutInfo.getPackage();
+        String badgePkg = getBadgePackage(shortcutInfo);
         boolean hasBadgePkgSet = !badgePkg.equals(shortcutInfo.getPackage());
         if (cn != null && !hasBadgePkgSet) {
             // Get the app info for the source activity.
@@ -223,4 +199,14 @@
             return pkgInfo;
         }
     }
+
+    private String getBadgePackage(ShortcutInfo si) {
+        String whitelistedPkg = mContext.getString(R.string.shortcutinfo_badgepkg_whitelist);
+        if (whitelistedPkg.equals(si.getPackage())
+                && si.getExtras() != null
+                && si.getExtras().containsKey(EXTRA_BADGEPKG)) {
+            return si.getExtras().getString(EXTRA_BADGEPKG);
+        }
+        return si.getPackage();
+    }
 }
diff --git a/src/com/android/launcher3/icons/ShortcutCachingLogic.java b/src/com/android/launcher3/icons/ShortcutCachingLogic.java
index 5c21470..5d696fd 100644
--- a/src/com/android/launcher3/icons/ShortcutCachingLogic.java
+++ b/src/com/android/launcher3/icons/ShortcutCachingLogic.java
@@ -23,14 +23,10 @@
 import android.graphics.drawable.Drawable;
 import android.os.UserHandle;
 
-import androidx.annotation.NonNull;
-
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.cache.CachingLogic;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.util.Themes;
 
 /**
  * Caching logic for shortcuts.
@@ -52,23 +48,20 @@
         return info.getShortLabel();
     }
 
-    @NonNull
     @Override
-    public BitmapInfo loadIcon(Context context, ShortcutInfo info) {
-        try (LauncherIcons li = LauncherIcons.obtain(context)) {
-            Drawable unbadgedDrawable = DeepShortcutManager.getInstance(context)
-                    .getShortcutIconDrawable(info, LauncherAppState.getIDP(context).fillResIconDpi);
-            if (unbadgedDrawable == null) return BitmapInfo.LOW_RES_INFO;
-            return new BitmapInfo(li.createScaledBitmapWithoutShadow(
-                    unbadgedDrawable, 0), Themes.getColorAccent(context));
+    public void loadIcon(Context context, ShortcutInfo info, BitmapInfo target) {
+        LauncherIcons li = LauncherIcons.obtain(context);
+        Drawable unbadgedDrawable = DeepShortcutManager.getInstance(context)
+                .getShortcutIconDrawable(info, LauncherAppState.getIDP(context).fillResIconDpi);
+        if (unbadgedDrawable != null) {
+            target.icon = li.createScaledBitmapWithoutShadow(unbadgedDrawable, 0);
         }
+        li.recycle();
     }
 
     @Override
     public long getLastUpdatedTime(ShortcutInfo shortcutInfo, PackageInfo info) {
-        if (shortcutInfo == null || !FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
-            return info.lastUpdateTime;
-        }
+        if (shortcutInfo == null) return info.lastUpdateTime;
         return Math.max(shortcutInfo.getLastChangedTimestamp(), info.lastUpdateTime);
     }
 
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
index fa0fe1b..227bb22 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -76,11 +76,6 @@
                     if (shortcutExists(dataModel, item.getIntent(), item.user)) {
                         continue;
                     }
-
-                    // b/139663018 Short-circuit this logic if the icon is a system app
-                    if (PackageManagerHelper.isSystemApp(app.getContext(), item.getIntent())) {
-                        continue;
-                    }
                 }
 
                 if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
@@ -122,39 +117,25 @@
                     }
                     SessionInfo sessionInfo = packageInstaller.getActiveSessionInfo(item.user,
                             packageName);
-                    List<LauncherActivityInfo> activities = launcherApps
-                            .getActivityList(packageName, item.user);
-                    boolean hasActivity = activities != null && !activities.isEmpty();
-
                     if (sessionInfo == null) {
-                        if (!hasActivity) {
+                        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 {
                             // 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();
-
-                        if (shortcutExists(dataModel, itemInfo.getIntent(), itemInfo.user)) {
-                            // We need this additional check here since we treat all auto added
-                            // workspace items as promise icons. At this point we now have the
-                            // correct intent to compare against existing workspace icons.
-                            // Icon already exists on the workspace and should not be auto-added.
-                            continue;
-                        }
-
-                        WorkspaceItemInfo wii = (WorkspaceItemInfo) itemInfo;
-                        wii.title = "";
-                        wii.bitmap = 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 a00a6bd..0a4f005 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 or if the main looper is already idle, there is no reason to wait
-        if (mCallbacks.get() == null || Looper.getMainLooper().getQueue().isIdle()) {
+        // If we are not binding, there is no reason to wait for idle.
+        if (mCallbacks.get() == null) {
             idleLock.queueIdle();
         }
         return idleLock;
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 95268d0..6154e7e 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -153,7 +153,7 @@
         info.title = getTitle();
         // the fallback icon
         if (!loadIcon(info)) {
-            info.bitmap = mIconCache.getDefaultIcon(info.user);
+            info.applyFrom(mIconCache.getDefaultIcon(info.user));
         }
 
         // TODO: If there's an explicit component and we can't install that, delete it.
@@ -183,7 +183,7 @@
                 info.iconResource.resourceName = resourceName;
                 BitmapInfo iconInfo = li.createIconBitmap(info.iconResource);
                 if (iconInfo != null) {
-                    info.bitmap = iconInfo;
+                    info.applyFrom(iconInfo);
                     return true;
                 }
             }
@@ -192,7 +192,7 @@
         // Failed to load from resource, try loading from DB.
         byte[] data = getBlob(iconIndex);
         try {
-            info.bitmap = li.createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length));
+            info.applyFrom(li.createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length)));
             return true;
         } catch (Exception e) {
             Log.e(TAG, "Failed to decode byte array for info " + info, e);
@@ -273,7 +273,7 @@
         info.intent = newIntent;
 
         mIconCache.getTitleAndIcon(info, lai, useLowResIcon);
-        if (mIconCache.isDefaultIcon(info.bitmap, user)) {
+        if (mIconCache.isDefaultIcon(info.iconBitmap, user)) {
             loadIcon(info);
         }
 
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 5893a08..81b701d 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -181,7 +181,16 @@
         }
 
         Object traceToken = TraceHelper.INSTANCE.beginSection(TAG);
-        TimingLogger logger = new TimingLogger(TAG, "run");
+        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");
         try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
             List<ShortcutInfo> allShortcuts = new ArrayList<>();
             loadWorkspace(allShortcuts);
@@ -216,12 +225,10 @@
                     mApp.getModel()::onPackageIconsUpdated);
             logger.addSplit("update icon cache");
 
-            if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
-                verifyNotStopped();
-                logger.addSplit("save shortcuts in icon cache");
-                updateHandler.updateIcons(allShortcuts, new ShortcutCachingLogic(),
-                        mApp.getModel()::onPackageIconsUpdated);
-            }
+            verifyNotStopped();
+            logger.addSplit("save shortcuts in icon cache");
+            updateHandler.updateIcons(allShortcuts, new ShortcutCachingLogic(),
+                    mApp.getModel()::onPackageIconsUpdated);
 
             // Take a break
             waitForIdle();
@@ -236,12 +243,10 @@
             mResults.bindDeepShortcuts();
             logger.addSplit("bindDeepShortcuts");
 
-            if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
-                verifyNotStopped();
-                logger.addSplit("save deep shortcuts in icon cache");
-                updateHandler.updateIcons(allDeepShortcuts,
-                        new ShortcutCachingLogic(), (pkgs, user) -> { });
-            }
+            verifyNotStopped();
+            logger.addSplit("save deep shortcuts in icon cache");
+            updateHandler.updateIcons(allDeepShortcuts,
+                    new ShortcutCachingLogic(), (pkgs, user) -> { });
 
             // Take a break
             waitForIdle();
@@ -531,9 +536,8 @@
                                     // use the last saved icon instead of the default.
                                     Supplier<ItemInfoWithIcon> fallbackIconProvider = () ->
                                             c.loadIcon(finalInfo, li) ? finalInfo : null;
-                                    info.bitmap = li.createShortcutIcon(
-                                            pinnedShortcut, true /* badged */,
-                                            fallbackIconProvider);
+                                    info.applyFrom(li.createShortcutIcon(pinnedShortcut,
+                                            true /* badged */, fallbackIconProvider));
                                     li.recycle();
                                     if (pmHelper.isAppSuspended(
                                             pinnedShortcut.getPackage(), info.user)) {
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 1e614bd..db63b7c 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -189,7 +189,7 @@
                             BitmapInfo iconInfo = li.createIconBitmap(si.iconResource);
                             li.recycle();
                             if (iconInfo != null) {
-                                si.bitmap = iconInfo;
+                                si.applyFrom(iconInfo);
                                 infoUpdated = true;
                             }
                         }
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java
index 05225d4..6c358b1 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.java
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.java
@@ -93,8 +93,8 @@
                     // If the shortcut is pinned but no longer has an icon in the system,
                     // keep the current icon instead of reverting to the default icon.
                     LauncherIcons li = LauncherIcons.obtain(context);
-                    workspaceItemInfo.bitmap = li.createShortcutIcon(
-                            fullDetails, true, () -> workspaceItemInfo);
+                    workspaceItemInfo.applyFrom(li.createShortcutIcon(fullDetails, true,
+                            () -> workspaceItemInfo));
                     li.recycle();
                     updatedWorkspaceItemInfos.add(workspaceItemInfo);
                 }
diff --git a/src/com/android/launcher3/model/UserLockStateChangedTask.java b/src/com/android/launcher3/model/UserLockStateChangedTask.java
index db1c307..4b773d7 100644
--- a/src/com/android/launcher3/model/UserLockStateChangedTask.java
+++ b/src/com/android/launcher3/model/UserLockStateChangedTask.java
@@ -92,7 +92,7 @@
                     // If the shortcut is pinned but no longer has an icon in the system,
                     // keep the current icon instead of reverting to the default icon.
                     LauncherIcons li = LauncherIcons.obtain(context);
-                    si.bitmap = li.createShortcutIcon(shortcut, true, () -> si);
+                    si.applyFrom(li.createShortcutIcon(shortcut, true, () -> si));
                     li.recycle();
                 } else {
                     si.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER;
diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java
index 021fb30..717a7e9 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.SingleAxisSwipeDetector.HORIZONTAL;
+import static com.android.launcher3.touch.SwipeDetector.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.SingleAxisSwipeDetector;
+import com.android.launcher3.touch.SwipeDetector;
 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 SingleAxisSwipeDetector mSwipeDetector;
+    private final SwipeDetector 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 SingleAxisSwipeDetector(mContext, mMainView, HORIZONTAL);
-        mSwipeDetector.setDetectableScrollConditions(SingleAxisSwipeDetector.DIRECTION_BOTH, false);
+        mSwipeDetector = new SwipeDetector(mContext, mMainView, HORIZONTAL);
+        mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, false);
         mMainView.setSwipeDetector(mSwipeDetector);
         mFooter.setContainer(this);
     }
diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java
index 059ad18..10378ee 100644
--- a/src/com/android/launcher3/notification/NotificationListener.java
+++ b/src/com/android/launcher3/notification/NotificationListener.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3.notification;
 
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
 
@@ -33,10 +32,9 @@
 import android.util.Log;
 import android.util.Pair;
 
-import androidx.annotation.AnyThread;
 import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
 
+import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.SecureSettingsObserver;
 
@@ -46,7 +44,6 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 
 /**
  * A {@link NotificationListenerService} that sends updates to its
@@ -62,17 +59,16 @@
     private static final int MSG_NOTIFICATION_POSTED = 1;
     private static final int MSG_NOTIFICATION_REMOVED = 2;
     private static final int MSG_NOTIFICATION_FULL_REFRESH = 3;
-    private static final int MSG_CANCEL_NOTIFICATION = 4;
-    private static final int MSG_RANKING_UPDATE = 5;
 
     private static NotificationListener sNotificationListenerInstance = null;
     private static NotificationsChangedListener sNotificationsChangedListener;
+    private static StatusBarNotificationsChangedListener sStatusBarNotificationsChangedListener;
     private static boolean sIsConnected;
+    private static boolean sIsCreated;
 
     private final Handler mWorkerHandler;
     private final Handler mUiHandler;
     private final Ranking mTempRanking = new Ranking();
-
     /** Maps groupKey's to the corresponding group of notifications. */
     private final Map<String, NotificationGroup> mNotificationGroupMap = new HashMap<>();
     /** Maps keys to their corresponding current group key */
@@ -83,12 +79,85 @@
 
     private SecureSettingsObserver mNotificationDotsObserver;
 
+    private final Handler.Callback mWorkerCallback = new Handler.Callback() {
+        @Override
+        public boolean handleMessage(Message message) {
+            switch (message.what) {
+                case MSG_NOTIFICATION_POSTED:
+                    mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
+                    break;
+                case MSG_NOTIFICATION_REMOVED:
+                    mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
+                    break;
+                case MSG_NOTIFICATION_FULL_REFRESH:
+                    List<StatusBarNotification> activeNotifications;
+                    if (sIsConnected) {
+                        try {
+                            activeNotifications = filterNotifications(getActiveNotifications());
+                        } catch (SecurityException ex) {
+                            Log.e(TAG, "SecurityException: failed to fetch notifications");
+                            activeNotifications = new ArrayList<StatusBarNotification>();
+
+                        }
+                    } else {
+                        activeNotifications = new ArrayList<StatusBarNotification>();
+                    }
+
+                    mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget();
+                    break;
+            }
+            return true;
+        }
+    };
+
+    private final Handler.Callback mUiCallback = new Handler.Callback() {
+        @Override
+        public boolean handleMessage(Message message) {
+            switch (message.what) {
+                case MSG_NOTIFICATION_POSTED:
+                    if (sNotificationsChangedListener != null) {
+                        NotificationPostedMsg msg = (NotificationPostedMsg) message.obj;
+                        sNotificationsChangedListener.onNotificationPosted(msg.packageUserKey,
+                                msg.notificationKey, msg.shouldBeFilteredOut);
+                    }
+                    break;
+                case MSG_NOTIFICATION_REMOVED:
+                    if (sNotificationsChangedListener != null) {
+                        Pair<PackageUserKey, NotificationKeyData> pair
+                                = (Pair<PackageUserKey, NotificationKeyData>) message.obj;
+                        sNotificationsChangedListener.onNotificationRemoved(pair.first, pair.second);
+                    }
+                    break;
+                case MSG_NOTIFICATION_FULL_REFRESH:
+                    if (sNotificationsChangedListener != null) {
+                        sNotificationsChangedListener.onNotificationFullRefresh(
+                                (List<StatusBarNotification>) message.obj);
+                    }
+                    break;
+            }
+            return true;
+        }
+    };
+
     public NotificationListener() {
-        mWorkerHandler = new Handler(MODEL_EXECUTOR.getLooper(), this::handleWorkerMessage);
-        mUiHandler = new Handler(Looper.getMainLooper(), this::handleUiMessage);
+        super();
+        mWorkerHandler = new Handler(MODEL_EXECUTOR.getLooper(), mWorkerCallback);
+        mUiHandler = new Handler(Looper.getMainLooper(), mUiCallback);
         sNotificationListenerInstance = this;
     }
 
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        sIsCreated = true;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        sIsCreated = false;
+    }
+
     public static @Nullable NotificationListener getInstanceIfConnected() {
         return sIsConnected ? sNotificationListenerInstance : null;
     }
@@ -99,107 +168,25 @@
         NotificationListener notificationListener = getInstanceIfConnected();
         if (notificationListener != null) {
             notificationListener.onNotificationFullRefresh();
-        } else {
+        } else if (!sIsCreated && sNotificationsChangedListener != null) {
             // User turned off dots globally, so we unbound this service;
             // tell the listener that there are no notifications to remove dots.
-            MODEL_EXECUTOR.submit(() -> MAIN_EXECUTOR.submit(() ->
-                            listener.onNotificationFullRefresh(Collections.emptyList())));
+            sNotificationsChangedListener.onNotificationFullRefresh(
+                    Collections.<StatusBarNotification>emptyList());
         }
     }
 
+    public static void setStatusBarNotificationsChangedListener
+            (StatusBarNotificationsChangedListener listener) {
+        sStatusBarNotificationsChangedListener = listener;
+    }
+
     public static void removeNotificationsChangedListener() {
         sNotificationsChangedListener = null;
     }
 
-    private boolean handleWorkerMessage(Message message) {
-        switch (message.what) {
-            case MSG_NOTIFICATION_POSTED: {
-                StatusBarNotification sbn = (StatusBarNotification) message.obj;
-                mUiHandler.obtainMessage(notificationIsValidForUI(sbn)
-                                ? MSG_NOTIFICATION_POSTED : MSG_NOTIFICATION_REMOVED,
-                        toKeyPair(sbn)).sendToTarget();
-                return true;
-            }
-            case MSG_NOTIFICATION_REMOVED: {
-                StatusBarNotification sbn = (StatusBarNotification) message.obj;
-                mUiHandler.obtainMessage(MSG_NOTIFICATION_REMOVED,
-                        toKeyPair(sbn)).sendToTarget();
-
-                NotificationGroup notificationGroup = mNotificationGroupMap.get(sbn.getGroupKey());
-                String key = sbn.getKey();
-                if (notificationGroup != null) {
-                    notificationGroup.removeChildKey(key);
-                    if (notificationGroup.isEmpty()) {
-                        if (key.equals(mLastKeyDismissedByLauncher)) {
-                            // Only cancel the group notification if launcher dismissed the
-                            // last child.
-                            cancelNotification(notificationGroup.getGroupSummaryKey());
-                        }
-                        mNotificationGroupMap.remove(sbn.getGroupKey());
-                    }
-                }
-                if (key.equals(mLastKeyDismissedByLauncher)) {
-                    mLastKeyDismissedByLauncher = null;
-                }
-                return true;
-            }
-            case MSG_NOTIFICATION_FULL_REFRESH:
-                List<StatusBarNotification> activeNotifications = null;
-                if (sIsConnected) {
-                    try {
-                        activeNotifications = Arrays.stream(getActiveNotifications())
-                                .filter(this::notificationIsValidForUI)
-                                .collect(Collectors.toList());
-                    } catch (SecurityException ex) {
-                        Log.e(TAG, "SecurityException: failed to fetch notifications");
-                        activeNotifications = new ArrayList<>();
-                    }
-                } else {
-                    activeNotifications = new ArrayList<>();
-                }
-
-                mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget();
-                return true;
-            case MSG_CANCEL_NOTIFICATION: {
-                mLastKeyDismissedByLauncher = (String) message.obj;
-                cancelNotification(mLastKeyDismissedByLauncher);
-                return true;
-            }
-            case MSG_RANKING_UPDATE: {
-                String[] keys = ((RankingMap) message.obj).getOrderedKeys();
-                for (StatusBarNotification sbn : getActiveNotifications(keys)) {
-                    updateGroupKeyIfNecessary(sbn);
-                }
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private boolean handleUiMessage(Message message) {
-        switch (message.what) {
-            case MSG_NOTIFICATION_POSTED:
-                if (sNotificationsChangedListener != null) {
-                    Pair<PackageUserKey, NotificationKeyData> msg = (Pair) message.obj;
-                    sNotificationsChangedListener.onNotificationPosted(
-                            msg.first, msg.second);
-                }
-                break;
-            case MSG_NOTIFICATION_REMOVED:
-                if (sNotificationsChangedListener != null) {
-                    Pair<PackageUserKey, NotificationKeyData> msg = (Pair) message.obj;
-                    sNotificationsChangedListener.onNotificationRemoved(
-                            msg.first, msg.second);
-                }
-                break;
-            case MSG_NOTIFICATION_FULL_REFRESH:
-                if (sNotificationsChangedListener != null) {
-                    sNotificationsChangedListener.onNotificationFullRefresh(
-                            (List<StatusBarNotification>) message.obj);
-                }
-                break;
-        }
-        return true;
+    public static void removeStatusBarNotificationsChangedListener() {
+        sStatusBarNotificationsChangedListener = null;
     }
 
     @Override
@@ -230,37 +217,84 @@
         super.onListenerDisconnected();
         sIsConnected = false;
         mNotificationDotsObserver.unregister();
-        onNotificationFullRefresh();
     }
 
     @Override
     public void onNotificationPosted(final StatusBarNotification sbn) {
-        if (sbn != null) {
-            mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, sbn).sendToTarget();
+        super.onNotificationPosted(sbn);
+        if (sbn == null) {
+            // There is a bug in platform where we can get a null notification; just ignore it.
+            return;
+        }
+        mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, new NotificationPostedMsg(sbn))
+            .sendToTarget();
+        if (sStatusBarNotificationsChangedListener != null) {
+            sStatusBarNotificationsChangedListener.onNotificationPosted(sbn);
+        }
+    }
+
+    /**
+     * An object containing data to send to MSG_NOTIFICATION_POSTED targets.
+     */
+    private class NotificationPostedMsg {
+        final PackageUserKey packageUserKey;
+        final NotificationKeyData notificationKey;
+        final boolean shouldBeFilteredOut;
+
+        NotificationPostedMsg(StatusBarNotification sbn) {
+            packageUserKey = PackageUserKey.fromNotification(sbn);
+            notificationKey = NotificationKeyData.fromNotification(sbn);
+            shouldBeFilteredOut = shouldBeFilteredOut(sbn);
         }
     }
 
     @Override
     public void onNotificationRemoved(final StatusBarNotification sbn) {
-        if (sbn != null) {
-            mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, sbn).sendToTarget();
+        super.onNotificationRemoved(sbn);
+        if (sbn == null) {
+            // There is a bug in platform where we can get a null notification; just ignore it.
+            return;
         }
+        Pair<PackageUserKey, NotificationKeyData> packageUserKeyAndNotificationKey
+            = new Pair<>(PackageUserKey.fromNotification(sbn),
+            NotificationKeyData.fromNotification(sbn));
+        mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, packageUserKeyAndNotificationKey)
+            .sendToTarget();
+        if (sStatusBarNotificationsChangedListener != null) {
+            sStatusBarNotificationsChangedListener.onNotificationRemoved(sbn);
+        }
+
+        NotificationGroup notificationGroup = mNotificationGroupMap.get(sbn.getGroupKey());
+        String key = sbn.getKey();
+        if (notificationGroup != null) {
+            notificationGroup.removeChildKey(key);
+            if (notificationGroup.isEmpty()) {
+                if (key.equals(mLastKeyDismissedByLauncher)) {
+                    // Only cancel the group notification if launcher dismissed the last child.
+                    cancelNotification(notificationGroup.getGroupSummaryKey());
+                }
+                mNotificationGroupMap.remove(sbn.getGroupKey());
+            }
+        }
+        if (key.equals(mLastKeyDismissedByLauncher)) {
+            mLastKeyDismissedByLauncher = null;
+        }
+    }
+
+    public void cancelNotificationFromLauncher(String key) {
+        mLastKeyDismissedByLauncher = key;
+        cancelNotification(key);
     }
 
     @Override
     public void onNotificationRankingUpdate(RankingMap rankingMap) {
-        mWorkerHandler.obtainMessage(MSG_RANKING_UPDATE, rankingMap).sendToTarget();
+        super.onNotificationRankingUpdate(rankingMap);
+        String[] keys = rankingMap.getOrderedKeys();
+        for (StatusBarNotification sbn : getActiveNotifications(keys)) {
+            updateGroupKeyIfNecessary(sbn);
+        }
     }
 
-    /**
-     * Cancels a notification
-     */
-    @AnyThread
-    public void cancelNotificationFromLauncher(String key) {
-        mWorkerHandler.obtainMessage(MSG_CANCEL_NOTIFICATION, key).sendToTarget();
-    }
-
-    @WorkerThread
     private void updateGroupKeyIfNecessary(StatusBarNotification sbn) {
         String childKey = sbn.getKey();
         String oldGroupKey = mNotificationGroupKeyMap.get(childKey);
@@ -294,33 +328,53 @@
         }
     }
 
-    /**
-     * This makes a potentially expensive binder call and should be run on a background thread.
-     */
-    @WorkerThread
+    /** This makes a potentially expensive binder call and should be run on a background thread. */
     public List<StatusBarNotification> getNotificationsForKeys(List<NotificationKeyData> keys) {
-        StatusBarNotification[] notifications = getActiveNotifications(
-                keys.stream().map(n -> n.notificationKey).toArray(String[]::new));
-        return notifications == null ? Collections.emptyList() : Arrays.asList(notifications);
+        StatusBarNotification[] notifications = NotificationListener.this
+                .getActiveNotifications(NotificationKeyData.extractKeysOnly(keys)
+                        .toArray(new String[keys.size()]));
+        return notifications == null
+                ? Collections.<StatusBarNotification>emptyList() : Arrays.asList(notifications);
     }
 
     /**
-     * Returns true for notifications that have an intent and are not headers for grouped
-     * notifications and should be shown in the notification popup.
+     * Filter out notifications that don't have an intent
+     * or are headers for grouped notifications.
+     *
+     * @see #shouldBeFilteredOut(StatusBarNotification)
      */
-    @WorkerThread
-    private boolean notificationIsValidForUI(StatusBarNotification sbn) {
+    private List<StatusBarNotification> filterNotifications(
+            StatusBarNotification[] notifications) {
+        if (notifications == null) return null;
+        IntSet removedNotifications = new IntSet();
+        for (int i = 0; i < notifications.length; i++) {
+            if (shouldBeFilteredOut(notifications[i])) {
+                removedNotifications.add(i);
+            }
+        }
+        List<StatusBarNotification> filteredNotifications = new ArrayList<>(
+                notifications.length - removedNotifications.size());
+        for (int i = 0; i < notifications.length; i++) {
+            if (!removedNotifications.contains(i)) {
+                filteredNotifications.add(notifications[i]);
+            }
+        }
+        return filteredNotifications;
+    }
+
+    private boolean shouldBeFilteredOut(StatusBarNotification sbn) {
         Notification notification = sbn.getNotification();
+
         updateGroupKeyIfNecessary(sbn);
 
         getCurrentRanking().getRanking(sbn.getKey(), mTempRanking);
         if (!mTempRanking.canShowBadge()) {
-            return false;
+            return true;
         }
         if (mTempRanking.getChannel().getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) {
             // Special filtering for the default, legacy "Miscellaneous" channel.
             if ((notification.flags & Notification.FLAG_ONGOING_EVENT) != 0) {
-                return false;
+                return true;
             }
         }
 
@@ -328,19 +382,19 @@
         CharSequence text = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
         boolean missingTitleAndText = TextUtils.isEmpty(title) && TextUtils.isEmpty(text);
         boolean isGroupHeader = (notification.flags & Notification.FLAG_GROUP_SUMMARY) != 0;
-        return !isGroupHeader && !missingTitleAndText;
-    }
-
-    private static Pair<PackageUserKey, NotificationKeyData> toKeyPair(StatusBarNotification sbn) {
-        return Pair.create(PackageUserKey.fromNotification(sbn),
-                NotificationKeyData.fromNotification(sbn));
+        return (isGroupHeader || missingTitleAndText);
     }
 
     public interface NotificationsChangedListener {
         void onNotificationPosted(PackageUserKey postedPackageUserKey,
-                NotificationKeyData notificationKey);
+                NotificationKeyData notificationKey, boolean shouldBeFilteredOut);
         void onNotificationRemoved(PackageUserKey removedPackageUserKey,
                 NotificationKeyData notificationKey);
         void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications);
     }
+
+    public interface StatusBarNotificationsChangedListener {
+        void onNotificationPosted(StatusBarNotification sbn);
+        void onNotificationRemoved(StatusBarNotification sbn);
+    }
 }
diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java
index b67adbb..78627ec 100644
--- a/src/com/android/launcher3/notification/NotificationMainView.java
+++ b/src/com/android/launcher3/notification/NotificationMainView.java
@@ -38,9 +38,8 @@
 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.SingleAxisSwipeDetector;
+import com.android.launcher3.touch.SwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.Themes;
 
@@ -49,7 +48,7 @@
  * e.g. icon + title + text.
  */
 @TargetApi(Build.VERSION_CODES.N)
-public class NotificationMainView extends FrameLayout implements SingleAxisSwipeDetector.Listener {
+public class NotificationMainView extends FrameLayout implements SwipeDetector.Listener {
 
     private static FloatProperty<NotificationMainView> CONTENT_TRANSLATION =
             new FloatProperty<NotificationMainView>("contentTranslation") {
@@ -76,7 +75,7 @@
     private TextView mTextView;
     private View mIconView;
 
-    private SingleAxisSwipeDetector mSwipeDetector;
+    private SwipeDetector mSwipeDetector;
 
     public NotificationMainView(Context context) {
         this(context, null, 0);
@@ -108,7 +107,7 @@
         mIconView = findViewById(R.id.popup_item_icon);
     }
 
-    public void setSwipeDetector(SingleAxisSwipeDetector swipeDetector) {
+    public void setSwipeDetector(SwipeDetector swipeDetector) {
         mSwipeDetector = swipeDetector;
     }
 
@@ -174,7 +173,7 @@
                 LauncherLogProto.ItemType.NOTIFICATION);
     }
 
-    // SingleAxisSwipeDetector.Listener's
+    // SwipeDetector.Listener's
     @Override
     public void onDragStart(boolean start) { }
 
@@ -188,7 +187,7 @@
     }
 
     @Override
-    public void onDragEnd(float velocity) {
+    public void onDragEnd(float velocity, boolean fling) {
         final boolean willExit;
         final float endTranslation;
         final float startTranslation = mTextAndBackground.getTranslationX();
@@ -196,7 +195,7 @@
         if (!canChildBeDismissed()) {
             willExit = false;
             endTranslation = 0;
-        } else if (mSwipeDetector.isFling(velocity)) {
+        } else if (fling) {
             willExit = true;
             endTranslation = velocity < 0 ? - getWidth() : getWidth();
         } else if (Math.abs(startTranslation) > getWidth() / 2) {
@@ -207,7 +206,7 @@
             endTranslation = 0;
         }
 
-        long duration = BaseSwipeDetector.calculateDuration(velocity,
+        long duration = SwipeDetector.calculateDuration(velocity,
                 (endTranslation - startTranslation) / getWidth());
 
         mContentTranslateAnimator.removeAllListeners();
diff --git a/src/com/android/launcher3/pm/PinRequestHelper.java b/src/com/android/launcher3/pm/PinRequestHelper.java
index 02f96a1..5b6b56d 100644
--- a/src/com/android/launcher3/pm/PinRequestHelper.java
+++ b/src/com/android/launcher3/pm/PinRequestHelper.java
@@ -31,8 +31,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.icons.LauncherIcons;
 
 public class PinRequestHelper {
 
@@ -82,15 +80,7 @@
             ShortcutInfo si = request.getShortcutInfo();
             WorkspaceItemInfo info = new WorkspaceItemInfo(si, context);
             // Apply the unbadged icon and fetch the actual icon asynchronously.
-            if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
-                fetchAndUpdateShortcutIconAsync(context, info, si, false);
-            } else {
-                LauncherIcons li = LauncherIcons.obtain(context);
-                info.bitmap = li.createShortcutIcon(si, false /* badged */);
-                li.recycle();
-                LauncherAppState.getInstance(context).getModel()
-                        .updateAndBindWorkspaceItem(info, si);
-            }
+            fetchAndUpdateShortcutIconAsync(context, info, si, false);
             return info;
         } else {
             return null;
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 98f7fd8..28000b9 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -360,14 +360,10 @@
         final TimeInterpolator revealInterpolator = ACCEL_DEACCEL;
 
         // Rectangular reveal.
-        mEndRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
         final ValueAnimator revealAnim = createOpenCloseOutlineProvider()
                 .createRevealAnimator(this, false);
         revealAnim.setDuration(revealDuration);
         revealAnim.setInterpolator(revealInterpolator);
-        // Clip the popup to the initial outline while the notification dot and arrow animate.
-        revealAnim.start();
-        revealAnim.pause();
 
         ValueAnimator fadeIn = ValueAnimator.ofFloat(0, 1);
         fadeIn.setDuration(revealDuration + arrowDuration);
@@ -403,6 +399,7 @@
         if (!mIsOpen) {
             return;
         }
+        mEndRect.setEmpty();
         if (getOutlineProvider() instanceof RevealOutlineAnimation) {
             ((RevealOutlineAnimation) getOutlineProvider()).getOutline(mEndRect);
         }
@@ -474,6 +471,9 @@
 
         mStartRect.set(arrowCenterX - halfArrowWidth, arrowCenterY, arrowCenterX + halfArrowWidth,
                 arrowCenterY);
+        if (mEndRect.isEmpty()) {
+            mEndRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
+        }
 
         return new RoundedRectRevealOutlineProvider
                 (arrowCornerRadius, mOutlineRadius, mStartRect, mEndRect);
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index e70673a..4833c26 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -77,7 +77,6 @@
 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.
@@ -197,6 +196,9 @@
      * @return the container if shown or null.
      */
     public static PopupContainerWithArrow showForIcon(BubbleTextView icon) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_CONTEXT_MENU, "showForIcon");
+        }
         Launcher launcher = Launcher.getLauncher(icon.getContext());
         if (getOpen(launcher) != null) {
             // There is already an items container open, so don't open this one.
@@ -211,7 +213,7 @@
         final PopupContainerWithArrow container =
                 (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
                         R.layout.popup_container, launcher.getDragLayer(), false);
-        container.populateAndShow(icon, itemInfo);
+        container.populateAndShow(icon, itemInfo, SystemShortcutFactory.INSTANCE.get(launcher));
         return container;
     }
 
@@ -236,15 +238,16 @@
         }
     }
 
-    protected void populateAndShow(BubbleTextView icon, ItemInfo item) {
+    protected void populateAndShow(
+            BubbleTextView icon, ItemInfo item, SystemShortcutFactory factory) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_CONTEXT_MENU, "populateAndShow");
+        }
         PopupDataProvider popupDataProvider = mLauncher.getPopupDataProvider();
         populateAndShow(icon,
                 popupDataProvider.getShortcutCountForItem(item),
                 popupDataProvider.getNotificationKeysForItem(item),
-                mLauncher.getSupportedShortcuts()
-                        .map(s -> s.getShortcut(mLauncher, item))
-                        .filter(s -> s != null)
-                        .collect(Collectors.toList()));
+                factory.getEnabledShortcuts(mLauncher, item));
     }
 
     public ViewGroup getSystemShortcutContainerForTesting() {
@@ -379,7 +382,8 @@
     @Override
     public void onWidgetsBound() {
         ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
-        SystemShortcut widgetInfo = SystemShortcut.WIDGETS.getShortcut(mLauncher, itemInfo);
+        SystemShortcut widgetInfo = new SystemShortcut.Widgets();
+        View.OnClickListener onClickListener = widgetInfo.getOnClickListener(mLauncher, itemInfo);
         View widgetsView = null;
         int count = mSystemShortcutContainer.getChildCount();
         for (int i = 0; i < count; i++) {
@@ -390,7 +394,7 @@
             }
         }
 
-        if (widgetInfo != null && widgetsView == null) {
+        if (onClickListener != null && widgetsView == null) {
             // We didn't have any widgets cached but now there are some, so enable the shortcut.
             if (mSystemShortcutContainer != this) {
                 initializeSystemShortcut(
@@ -403,7 +407,7 @@
                 close(false);
                 PopupContainerWithArrow.showForIcon(mOriginalIcon);
             }
-        } else if (widgetInfo == null && widgetsView != null) {
+        } else if (onClickListener == null && widgetsView != null) {
             // No widgets exist, but we previously added the shortcut so remove it.
             if (mSystemShortcutContainer != this) {
                 mSystemShortcutContainer.removeView(widgetsView);
@@ -426,7 +430,8 @@
             info.setIconAndContentDescriptionFor((ImageView) view);
         }
         view.setTag(info);
-        view.setOnClickListener(info);
+        view.setOnClickListener(info.getOnClickListener(mLauncher,
+                (ItemInfo) mOriginalIcon.getTag()));
     }
 
     /**
@@ -501,7 +506,7 @@
         DotInfo dotInfo = mLauncher.getDotInfoForItem(itemInfo);
         if (mNotificationItemView != null && dotInfo != null) {
             mNotificationItemView.updateHeader(
-                    dotInfo.getNotificationCount(), itemInfo.bitmap.color);
+                    dotInfo.getNotificationCount(), itemInfo.iconColor);
         }
     }
 
@@ -570,11 +575,8 @@
 
     @Override
     protected void closeComplete() {
-        PopupContainerWithArrow openPopup = getOpen(mLauncher);
-        if (openPopup == null || openPopup.mOriginalIcon != mOriginalIcon) {
-            mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible());
-            mOriginalIcon.setForceHideDot(false);
-        }
+        mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible());
+        mOriginalIcon.setForceHideDot(false);
         super.closeComplete();
     }
 
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index c5aa836..4612b2a 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -20,15 +20,13 @@
 import android.service.notification.StatusBarNotification;
 import android.util.Log;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.notification.NotificationKeyData;
 import com.android.launcher3.notification.NotificationListener;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.ShortcutUtil;
@@ -41,9 +39,13 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 /**
  * Provides data for the popup menu that appears after long-clicking on apps.
  */
@@ -74,14 +76,28 @@
 
     @Override
     public void onNotificationPosted(PackageUserKey postedPackageUserKey,
-            NotificationKeyData notificationKey) {
+            NotificationKeyData notificationKey, boolean shouldBeFilteredOut) {
         DotInfo dotInfo = mPackageUserToDotInfos.get(postedPackageUserKey);
+        boolean dotShouldBeRefreshed;
         if (dotInfo == null) {
-            dotInfo = new DotInfo();
-            mPackageUserToDotInfos.put(postedPackageUserKey, dotInfo);
+            if (!shouldBeFilteredOut) {
+                DotInfo newDotInfo = new DotInfo();
+                newDotInfo.addOrUpdateNotificationKey(notificationKey);
+                mPackageUserToDotInfos.put(postedPackageUserKey, newDotInfo);
+                dotShouldBeRefreshed = true;
+            } else {
+                dotShouldBeRefreshed = false;
+            }
+        } else {
+            dotShouldBeRefreshed = shouldBeFilteredOut
+                    ? dotInfo.removeNotificationKey(notificationKey)
+                    : dotInfo.addOrUpdateNotificationKey(notificationKey);
+            if (dotInfo.getNotificationKeys().size() == 0) {
+                mPackageUserToDotInfos.remove(postedPackageUserKey);
+            }
         }
-        if (dotInfo.addOrUpdateNotificationKey(notificationKey)) {
-            updateNotificationDots(postedPackageUserKey::equals);
+        if (dotShouldBeRefreshed) {
+            updateNotificationDots(t -> postedPackageUserKey.equals(t));
         }
     }
 
@@ -93,7 +109,7 @@
             if (oldDotInfo.getNotificationKeys().size() == 0) {
                 mPackageUserToDotInfos.remove(removedPackageUserKey);
             }
-            updateNotificationDots(removedPackageUserKey::equals);
+            updateNotificationDots(t -> removedPackageUserKey.equals(t));
             trimNotifications(mPackageUserToDotInfos);
         }
     }
@@ -179,6 +195,14 @@
                 : getNotificationsForItem(info, dotInfo.getNotificationKeys());
     }
 
+    /** This makes a potentially expensive binder call and should be run on a background thread. */
+    public @NonNull List<StatusBarNotification> getStatusBarNotificationsForKeys(
+            List<NotificationKeyData> notificationKeys) {
+        NotificationListener notificationListener = NotificationListener.getInstanceIfConnected();
+        return notificationListener == null ? Collections.EMPTY_LIST
+                : notificationListener.getNotificationsForKeys(notificationKeys);
+    }
+
     public void cancelNotification(String notificationKey) {
         NotificationListener notificationListener = NotificationListener.getInstanceIfConnected();
         if (notificationListener == null) {
diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java
index 80c6683..dbfe988 100644
--- a/src/com/android/launcher3/popup/PopupPopulator.java
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -20,9 +20,7 @@
 import android.content.pm.ShortcutInfo;
 import android.os.Handler;
 import android.os.UserHandle;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
+import android.service.notification.StatusBarNotification;
 
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
@@ -30,7 +28,6 @@
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.notification.NotificationInfo;
 import com.android.launcher3.notification.NotificationKeyData;
-import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.util.PackageUserKey;
@@ -40,7 +37,9 @@
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
-import java.util.stream.Collectors;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 /**
  * Contains logic relevant to populating a {@link PopupContainerWithArrow}. In particular,
@@ -131,15 +130,12 @@
         final UserHandle user = originalInfo.user;
         return () -> {
             if (!notificationKeys.isEmpty()) {
-                NotificationListener notificationListener =
-                        NotificationListener.getInstanceIfConnected();
-                final List<NotificationInfo> infos;
-                if (notificationListener == null) {
-                    infos = Collections.emptyList();
-                } else {
-                    infos = notificationListener.getNotificationsForKeys(notificationKeys).stream()
-                            .map(sbn -> new NotificationInfo(launcher, sbn))
-                            .collect(Collectors.toList());
+                List<StatusBarNotification> notifications = launcher.getPopupDataProvider()
+                        .getStatusBarNotificationsForKeys(notificationKeys);
+                List<NotificationInfo> infos = new ArrayList<>(notifications.size());
+                for (int i = 0; i < notifications.size(); i++) {
+                    StatusBarNotification notification = notifications.get(i);
+                    infos.add(new NotificationInfo(launcher, notification));
                 }
                 uiHandler.post(() -> container.applyNotificationInfos(infos));
             }
@@ -154,7 +150,7 @@
                 final WorkspaceItemInfo si = new WorkspaceItemInfo(shortcut, launcher);
                 // Use unbadged icon for the menu.
                 LauncherIcons li = LauncherIcons.obtain(launcher);
-                si.bitmap = li.createShortcutIcon(shortcut, false /* badged */);
+                si.applyFrom(li.createShortcutIcon(shortcut, false /* badged */));
                 li.recycle();
                 si.rank = i;
 
diff --git a/src/com/android/launcher3/popup/RemoteActionShortcut.java b/src/com/android/launcher3/popup/RemoteActionShortcut.java
index 8751202..5a5fbab 100644
--- a/src/com/android/launcher3/popup/RemoteActionShortcut.java
+++ b/src/com/android/launcher3/popup/RemoteActionShortcut.java
@@ -16,19 +16,13 @@
 
 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.Build;
+import android.os.Handler;
+import android.os.Looper;
 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;
@@ -38,75 +32,55 @@
 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,
-            BaseDraggingActivity activity, ItemInfo itemInfo) {
-        super(0, R.id.action_remote_action_shortcut, activity, itemInfo);
+    public RemoteActionShortcut(RemoteAction action) {
+        super(action.getIcon(), action.getTitle(), action.getContentDescription(),
+                R.id.action_remote_action_shortcut);
         mAction = action;
     }
 
     @Override
-    public void setIconAndLabelFor(View iconView, TextView labelView) {
-        mAction.getIcon().loadDrawableAsync(iconView.getContext(),
-                iconView::setBackground,
-                MAIN_EXECUTOR.getHandler());
-        labelView.setText(mAction.getTitle());
-    }
+    public View.OnClickListener getOnClickListener(
+            final BaseDraggingActivity activity, final ItemInfo itemInfo) {
+        return view -> {
+            AbstractFloatingView.closeAllOpenViews(activity);
 
-    @Override
-    public void setIconAndContentDescriptionFor(ImageView view) {
-        mAction.getIcon().loadDrawableAsync(view.getContext(),
-                view::setImageDrawable,
-                MAIN_EXECUTOR.getHandler());
-        view.setContentDescription(mAction.getContentDescription());
-    }
+            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 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);
+            activity.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 222c6c9..a87b7b8 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -5,13 +5,14 @@
 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;
@@ -38,30 +39,41 @@
  * Example system shortcuts, defined as inner classes, include Widgets and AppInfo.
  * @param <T>
  */
-public abstract class SystemShortcut<T extends BaseDraggingActivity> extends ItemInfo
-        implements View.OnClickListener {
-
+public abstract class SystemShortcut<T extends BaseDraggingActivity>
+        extends ItemInfo {
     private final int mIconResId;
     private final int mLabelResId;
+    private final Icon mIcon;
+    private final CharSequence mLabel;
+    private final CharSequence mContentDescription;
     private final int mAccessibilityActionId;
 
-    protected final T mTarget;
-    protected final ItemInfo mItemInfo;
-
-    public SystemShortcut(int iconResId, int labelResId, T target, ItemInfo itemInfo) {
+    public SystemShortcut(int iconResId, int labelResId) {
         mIconResId = iconResId;
         mLabelResId = labelResId;
         mAccessibilityActionId = labelResId;
-        mTarget = target;
-        mItemInfo = itemInfo;
+        mIcon = null;
+        mLabel = null;
+        mContentDescription = null;
     }
 
-    public SystemShortcut(SystemShortcut<T> other) {
+    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) {
         mIconResId = other.mIconResId;
         mLabelResId = other.mLabelResId;
+        mIcon = other.mIcon;
+        mLabel = other.mLabel;
+        mContentDescription = other.mContentDescription;
         mAccessibilityActionId = other.mAccessibilityActionId;
-        mTarget = other.mTarget;
-        mItemInfo = other.mItemInfo;
     }
 
     /**
@@ -72,135 +84,150 @@
     }
 
     public void setIconAndLabelFor(View iconView, TextView labelView) {
-        iconView.setBackgroundResource(mIconResId);
-        labelView.setText(mLabelResId);
+        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);
+        }
     }
 
     public void setIconAndContentDescriptionFor(ImageView view) {
-        view.setImageResource(mIconResId);
-        view.setContentDescription(view.getContext().getText(mLabelResId));
+        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);
     }
 
     public AccessibilityNodeInfo.AccessibilityAction createAccessibilityAction(Context context) {
-        return new AccessibilityNodeInfo.AccessibilityAction(
-                mAccessibilityActionId, context.getText(mLabelResId));
+        return new AccessibilityNodeInfo.AccessibilityAction(mAccessibilityActionId,
+                getContentDescription(context));
     }
 
     public boolean hasHandlerForAction(int action) {
         return mAccessibilityActionId == action;
     }
 
-    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 abstract View.OnClickListener getOnClickListener(T activity, ItemInfo itemInfo);
 
     public static class Widgets extends SystemShortcut<Launcher> {
 
-        public Widgets(Launcher target, ItemInfo itemInfo) {
-            super(R.drawable.ic_widget, R.string.widget_button_text, target, itemInfo);
+        public Widgets() {
+            super(R.drawable.ic_widget, R.string.widget_button_text);
         }
 
         @Override
-        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 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 static final Factory<BaseDraggingActivity> APP_INFO = AppInfo::new;
-
     public static class AppInfo extends SystemShortcut {
-
-        public AppInfo(BaseDraggingActivity target, ItemInfo itemInfo) {
-            super(R.drawable.ic_info_no_shadow, R.string.app_info_drop_target_label, target,
-                    itemInfo);
+        public AppInfo() {
+            super(R.drawable.ic_info_no_shadow, R.string.app_info_drop_target_label);
         }
 
         @Override
-        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 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 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(BaseDraggingActivity target, ItemInfo itemInfo) {
-            super(R.drawable.ic_install_no_shadow, R.string.install_drop_target_label,
-                    target, itemInfo);
+        public Install() {
+            super(R.drawable.ic_install_no_shadow, R.string.install_drop_target_label);
         }
 
         @Override
-        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 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 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(Launcher launcher, ItemInfo itemInfo) {
-            super(R.drawable.ic_remove_no_shadow, R.string.dismiss_prediction_label, launcher,
-                    itemInfo);
+        public DismissPrediction() {
+            super(R.drawable.ic_remove_no_shadow, R.string.dismiss_prediction_label);
         }
 
         @Override
-        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);
+        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 static void dismissTaskMenuView(BaseDraggingActivity activity) {
+    protected 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
new file mode 100644
index 0000000..dfcc2f8
--- /dev/null
+++ b/src/com/android/launcher3/popup/SystemShortcutFactory.java
@@ -0,0 +1,61 @@
+/*
+ * 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/shortcuts/ShortcutDragPreviewProvider.java b/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java
index 8dd90e4..408ced2 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java
@@ -25,7 +25,6 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.DragPreviewProvider;
 import com.android.launcher3.icons.BitmapRenderer;
 
@@ -43,36 +42,16 @@
 
     @Override
     public Bitmap createDragBitmap() {
-        if (FeatureFlags.ENABLE_DEEP_SHORTCUT_ICON_CACHE.get()) {
-            int size = Launcher.getLauncher(mView.getContext()).getDeviceProfile().iconSizePx;
-            return BitmapRenderer.createHardwareBitmap(
-                    size + blurSizeOutline,
-                    size + blurSizeOutline,
-                    (c) -> drawDragViewOnBackground(c, size));
-        } else {
-            return createDragBitmapLegacy();
-        }
-    }
-
-    private Bitmap createDragBitmapLegacy() {
-        Drawable d = mView.getBackground();
-        Rect bounds = getDrawableBounds(d);
         int size = Launcher.getLauncher(mView.getContext()).getDeviceProfile().iconSizePx;
         return BitmapRenderer.createHardwareBitmap(
                 size + blurSizeOutline,
                 size + blurSizeOutline,
-                Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(b);
-        canvas.translate(blurSizeOutline / 2, blurSizeOutline / 2);
-        canvas.scale(size / bounds.width(), size / bounds.height(), 0, 0);
-        canvas.translate(bounds.left, bounds.top);
-        d.draw(canvas);
+                (c) -> drawDragViewOnBackground(c, size));
     }
 
     private void drawDragViewOnBackground(Canvas canvas, float size) {
         Drawable d = mView.getBackground();
         Rect bounds = getDrawableBounds(d);
-
         canvas.translate(blurSizeOutline / 2, blurSizeOutline / 2);
         canvas.scale(size / bounds.width(), size / bounds.height(), 0, 0);
         canvas.translate(bounds.left, bounds.top);
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 64df384..d0e648f 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -24,10 +24,9 @@
 import android.graphics.Color;
 import android.os.Bundle;
 import android.os.Debug;
+import android.util.Log;
 import android.view.View;
 
-import androidx.annotation.Keep;
-
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Launcher;
@@ -178,6 +177,11 @@
     }
 
     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();
     }
@@ -187,22 +191,6 @@
         Runtime.getRuntime().runFinalization();
 
         final CountDownLatch fence = new CountDownLatch(1);
-        createFinalizationObserver(fence);
-        try {
-            do {
-                Runtime.getRuntime().gc();
-                Runtime.getRuntime().runFinalization();
-            } while (!fence.await(100, TimeUnit.MILLISECONDS));
-        } catch (InterruptedException ex) {
-            throw new RuntimeException(ex);
-        }
-    }
-
-    // Create the observer in the scope of a method to minimize the chance that
-    // it remains live in a DEX/machine register at the point of the fence guard.
-    // This must be kept to avoid R8 inlining it.
-    @Keep
-    private static void createFinalizationObserver(CountDownLatch fence) {
         new Object() {
             @Override
             protected void finalize() throws Throwable {
@@ -213,5 +201,13 @@
                 }
             }
         };
+        try {
+            do {
+                Runtime.getRuntime().gc();
+                Runtime.getRuntime().runFinalization();
+            } while (!fence.await(100, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException ex) {
+            throw new RuntimeException(ex);
+        }
     }
 }
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 1cfa4af..923c466 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -84,4 +84,7 @@
     public static final String NO_BACKGROUND_TO_OVERVIEW_TAG = "b/138251824";
     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 60f6ee9..c5ba5ba 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, SingleAxisSwipeDetector.Listener {
+        implements TouchController, SwipeDetector.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 SingleAxisSwipeDetector mDetector;
-    protected final SingleAxisSwipeDetector.Direction mSwipeDirection;
+    protected final SwipeDetector mDetector;
+    protected final SwipeDetector.Direction mSwipeDirection;
 
     private boolean mNoIntercept;
     private boolean mIsLogContainerSet;
@@ -101,9 +101,9 @@
 
     private float mAtomicComponentsStartProgress;
 
-    public AbstractStateChangeTouchController(Launcher l, SingleAxisSwipeDetector.Direction dir) {
+    public AbstractStateChangeTouchController(Launcher l, SwipeDetector.Direction dir) {
         mLauncher = l;
-        mDetector = new SingleAxisSwipeDetector(l, this, dir);
+        mDetector = new SwipeDetector(l, this, dir);
         mSwipeDirection = dir;
     }
 
@@ -127,7 +127,7 @@
             boolean ignoreSlopWhenSettling = false;
 
             if (mCurrentAnimation != null) {
-                directionsToDetectScroll = SingleAxisSwipeDetector.DIRECTION_BOTH;
+                directionsToDetectScroll = SwipeDetector.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 |= SingleAxisSwipeDetector.DIRECTION_POSITIVE;
+            swipeDirection |= SwipeDetector.DIRECTION_POSITIVE;
         }
         if (getTargetState(fromState, false /* isDragTowardPositive */) != fromState) {
-            swipeDirection |= SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
+            swipeDirection |= SwipeDetector.DIRECTION_NEGATIVE;
         }
         return swipeDirection;
     }
@@ -369,8 +369,7 @@
     }
 
     @Override
-    public void onDragEnd(float velocity) {
-        boolean fling = mDetector.isFling(velocity);
+    public void onDragEnd(float velocity, boolean fling) {
         final int logAction = fling ? Touch.FLING : Touch.SWIPE;
 
         boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
@@ -407,7 +406,7 @@
             } else {
                 startProgress = Utilities.boundToRange(progress
                         + velocity * getSingleFrameMs(mLauncher) * mProgressMultiplier, 0f, 1f);
-                duration = BaseSwipeDetector.calculateDuration(velocity,
+                duration = SwipeDetector.calculateDuration(velocity,
                         endProgress - Math.max(progress, 0)) * durationMultiplier;
             }
         } else {
@@ -425,7 +424,7 @@
             } else {
                 startProgress = Utilities.boundToRange(progress
                         + velocity * getSingleFrameMs(mLauncher) * mProgressMultiplier, 0f, 1f);
-                duration = BaseSwipeDetector.calculateDuration(velocity,
+                duration = SwipeDetector.calculateDuration(velocity,
                         Math.min(progress, 1) - endProgress) * durationMultiplier;
             }
         }
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
deleted file mode 100644
index 12ca5ee..0000000
--- a/src/com/android/launcher3/touch/BaseSwipeDetector.java
+++ /dev/null
@@ -1,268 +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.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
- * @see BothAxesSwipeDetector
- */
-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/BothAxesSwipeDetector.java b/src/com/android/launcher3/touch/BothAxesSwipeDetector.java
deleted file mode 100644
index 944391e..0000000
--- a/src/com/android/launcher3/touch/BothAxesSwipeDetector.java
+++ /dev/null
@@ -1,99 +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.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;
-
-/**
- * Two dimensional scroll/drag/swipe gesture detector that reports x and y displacement/velocity.
- */
-public class BothAxesSwipeDetector extends BaseSwipeDetector {
-
-    public static final int DIRECTION_UP = 1 << 0;
-    // Note that this will track left instead of right in RTL.
-    public static final int DIRECTION_RIGHT = 1 << 1;
-    public static final int DIRECTION_DOWN = 1 << 2;
-    // Note that this will track right instead of left in RTL.
-    public static final int DIRECTION_LEFT = 1 << 3;
-
-    /* Client of this gesture detector can register a callback. */
-    private final Listener mListener;
-
-    private int mScrollDirections;
-
-    public BothAxesSwipeDetector(@NonNull Context context, @NonNull Listener l) {
-        this(ViewConfiguration.get(context), l, Utilities.isRtl(context.getResources()));
-    }
-
-    @VisibleForTesting
-    protected BothAxesSwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
-            boolean isRtl) {
-        super(config, isRtl);
-        mListener = l;
-    }
-
-    public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
-        mScrollDirections = scrollDirectionFlags;
-        mIgnoreSlopWhenSettling = ignoreSlop;
-    }
-
-    @Override
-    protected boolean shouldScrollStart(PointF displacement) {
-        // Check if the client is interested in scroll in current direction.
-        boolean canScrollUp = (mScrollDirections & DIRECTION_UP) > 0
-                && displacement.y <= -mTouchSlop;
-        boolean canScrollRight = (mScrollDirections & DIRECTION_RIGHT) > 0
-                && displacement.x >= mTouchSlop;
-        boolean canScrollDown = (mScrollDirections & DIRECTION_DOWN) > 0
-                && displacement.y >= mTouchSlop;
-        boolean canScrollLeft = (mScrollDirections & DIRECTION_LEFT) > 0
-                && displacement.x <= -mTouchSlop;
-        return canScrollUp || canScrollRight || canScrollDown || canScrollLeft;
-    }
-
-    @Override
-    protected void reportDragStartInternal(boolean recatch) {
-        mListener.onDragStart(!recatch);
-    }
-
-    @Override
-    protected void reportDraggingInternal(PointF displacement, MotionEvent event) {
-        mListener.onDrag(displacement, event);
-    }
-
-    @Override
-    protected void reportDragEndInternal(PointF velocity) {
-        mListener.onDragEnd(velocity);
-    }
-
-    /** Listener to receive updates on the swipe. */
-    public interface Listener {
-        /** @param start whether this was the original drag start, as opposed to a recatch. */
-        void onDragStart(boolean start);
-
-        boolean onDrag(PointF displacement, MotionEvent motionEvent);
-
-        void onDragEnd(PointF velocity);
-    }
-}
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index aa02d0a..86d2b39 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -79,10 +79,19 @@
     }
 
     private static boolean onAllAppsItemLongClick(View v) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_CONTEXT_MENU, "onAllAppsItemLongClick1");
+        }
         Launcher launcher = Launcher.getLauncher(v.getContext());
         if (!canStartDrag(launcher)) return false;
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_CONTEXT_MENU, "onAllAppsItemLongClick2");
+        }
         // When we have exited all apps or are in transition, disregard long clicks
         if (!launcher.isInState(ALL_APPS) && !launcher.isInState(OVERVIEW)) return false;
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_CONTEXT_MENU, "onAllAppsItemLongClick3");
+        }
         if (launcher.getWorkspace().isSwitchingState()) return false;
 
         // Start the drag
diff --git a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java b/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
deleted file mode 100644
index f2ebc45..0000000
--- a/src/com/android/launcher3/touch/SingleAxisSwipeDetector.java
+++ /dev/null
@@ -1,190 +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.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 {
-        /** @param start whether this was the original drag start, as opposed to a recatch. */
-        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
new file mode 100644
index 0000000..c38ca24
--- /dev/null
+++ b/src/com/android/launcher3/touch/SwipeDetector.java
@@ -0,0 +1,391 @@
+/*
+ * 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 499f655..b4f361f 100644
--- a/src/com/android/launcher3/util/ActivityTracker.java
+++ b/src/com/android/launcher3/util/ActivityTracker.java
@@ -45,46 +45,33 @@
     }
 
     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();
         }
     }
 
-    /**
-     * Schedules the callback to be notified when the activity is created.
-     * @return true if the activity is already created, false otherwise
-     */
-    public boolean schedule(SchedulerCallback<? extends T> callback) {
+    public void schedule(SchedulerCallback<? extends T> callback) {
         synchronized (this) {
             mPendingCallback = new WeakReference<>((SchedulerCallback<T>) callback);
         }
-        if (!notifyInitIfPending()) {
-            // If the activity doesn't already exist, then post and wait for the activity to start
-            MAIN_EXECUTOR.execute(this);
-            return false;
-        }
-        return true;
+        MAIN_EXECUTOR.execute(this);
     }
 
     @Override
     public void run() {
-        notifyInitIfPending();
-    }
-
-    /**
-     * Notifies the pending callback if the activity is now created.
-     * @return true if the activity is now created.
-     */
-    private boolean notifyInitIfPending() {
         T activity = mCurrentActivity.get();
         if (activity != null) {
-            notifyInitIfPending(activity, activity.isStarted());
-            return true;
+            initIfPending(activity, activity.isStarted());
         }
-        return false;
     }
 
-    public boolean notifyInitIfPending(T activity, boolean alreadyOnHome) {
+    public boolean initIfPending(T activity, boolean alreadyOnHome) {
         SchedulerCallback<T> pendingCallback = mPendingCallback.get();
         if (pendingCallback != null) {
             if (!pendingCallback.init(activity, alreadyOnHome)) {
@@ -110,6 +97,10 @@
     }
 
     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);
     }
@@ -133,7 +124,7 @@
             }
         }
         if (!result && !explicitIntent) {
-            result = notifyInitIfPending(activity, alreadyOnHome);
+            result = initIfPending(activity, alreadyOnHome);
         }
         return result;
     }
diff --git a/src/com/android/launcher3/util/ContentWriter.java b/src/com/android/launcher3/util/ContentWriter.java
index 2d64353..00adf10 100644
--- a/src/com/android/launcher3/util/ContentWriter.java
+++ b/src/com/android/launcher3/util/ContentWriter.java
@@ -26,7 +26,6 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.compat.UserManagerCompat;
-import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.GraphicsUtils;
 
 /**
@@ -38,7 +37,7 @@
     private final Context mContext;
 
     private CommitParams mCommitParams;
-    private BitmapInfo mIcon;
+    private Bitmap mIcon;
     private UserHandle mUser;
 
     public ContentWriter(Context context, CommitParams commitParams) {
@@ -80,7 +79,7 @@
         return this;
     }
 
-    public ContentWriter putIcon(BitmapInfo value, UserHandle user) {
+    public ContentWriter putIcon(Bitmap value, UserHandle user) {
         mIcon = value;
         mUser = user;
         return this;
@@ -98,7 +97,7 @@
         Preconditions.assertNonUiThread();
         if (mIcon != null && !LauncherAppState.getInstance(context).getIconCache()
                 .isDefaultIcon(mIcon, mUser)) {
-            mValues.put(LauncherSettings.Favorites.ICON, GraphicsUtils.flattenBitmap(mIcon.icon));
+            mValues.put(LauncherSettings.Favorites.ICON, GraphicsUtils.flattenBitmap(mIcon));
             mIcon = null;
         }
         return mValues;
diff --git a/src/com/android/launcher3/util/LooperIdleLock.java b/src/com/android/launcher3/util/LooperIdleLock.java
index f4ccf42..2896535 100644
--- a/src/com/android/launcher3/util/LooperIdleLock.java
+++ b/src/com/android/launcher3/util/LooperIdleLock.java
@@ -22,30 +22,29 @@
 /**
  * Utility class to block execution until the UI looper is idle.
  */
-public class LooperIdleLock implements MessageQueue.IdleHandler {
+public class LooperIdleLock implements MessageQueue.IdleHandler, Runnable {
 
     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/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 91f687e..7b4e0c6 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -274,9 +274,6 @@
         } else {
             packageName = cn.getPackageName();
         }
-        if (packageName == null) {
-            packageName = intent.getPackage();
-        }
         if (packageName != null) {
             try {
                 PackageInfo info = pm.getPackageInfo(packageName, 0);
diff --git a/src/com/android/launcher3/util/ShortcutUtil.java b/src/com/android/launcher3/util/ShortcutUtil.java
index a03b743..a69cd6c 100644
--- a/src/com/android/launcher3/util/ShortcutUtil.java
+++ b/src/com/android/launcher3/util/ShortcutUtil.java
@@ -27,6 +27,7 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.shortcuts.ShortcutKey;
@@ -76,12 +77,17 @@
     public static void fetchAndUpdateShortcutIconAsync(
             @NonNull Context context, @NonNull WorkspaceItemInfo info, @NonNull ShortcutInfo si,
             boolean badged) {
+        if (info.iconBitmap == null) {
+            // use low res icon as placeholder while the actual icon is being fetched.
+            info.iconBitmap = BitmapInfo.LOW_RES_ICON;
+            info.iconColor = Themes.getColorAccent(context);
+        }
         MODEL_EXECUTOR.execute(() -> {
-            try (LauncherIcons li = LauncherIcons.obtain(context)) {
-                info.bitmap = li.createShortcutIcon(si, badged, null);
-                LauncherAppState.getInstance(context).getModel()
-                        .updateAndBindWorkspaceItem(info, si);
-            }
+            LauncherIcons li = LauncherIcons.obtain(context);
+            BitmapInfo bitmapInfo = li.createShortcutIcon(si, badged, true, null);
+            info.applyFrom(bitmapInfo);
+            li.recycle();
+            LauncherAppState.getInstance(context).getModel().updateAndBindWorkspaceItem(info, si);
         });
     }
 
diff --git a/src/com/android/launcher3/util/UiThreadHelper.java b/src/com/android/launcher3/util/UiThreadHelper.java
index ec87e79..a133f01 100644
--- a/src/com/android/launcher3/util/UiThreadHelper.java
+++ b/src/com/android/launcher3/util/UiThreadHelper.java
@@ -23,6 +23,7 @@
 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
deleted file mode 100644
index 04741a1..0000000
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ /dev/null
@@ -1,84 +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.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 5a131c8..61ba4e5 100644
--- a/src/com/android/launcher3/util/ViewOnDrawExecutor.java
+++ b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
@@ -55,9 +55,7 @@
             mLoadAnimationCompleted = true;
         }
 
-        if (mAttachedView.isAttachedToWindow()) {
-            attachObserver();
-        }
+        attachObserver();
     }
 
     private void attachObserver() {
diff --git a/src/com/android/launcher3/util/ViewPool.java b/src/com/android/launcher3/util/ViewPool.java
index 5b33f18..8af048d 100644
--- a/src/com/android/launcher3/util/ViewPool.java
+++ b/src/com/android/launcher3/util/ViewPool.java
@@ -21,12 +21,12 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import com.android.launcher3.util.ViewPool.Reusable;
+
 import androidx.annotation.AnyThread;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
-import com.android.launcher3.util.ViewPool.Reusable;
-
 /**
  * Utility class to maintain a pool of reusable views.
  * During initialization, views are inflated on the background thread.
@@ -58,18 +58,14 @@
         Preconditions.assertUIThread();
         Handler handler = new Handler();
 
-        // LayoutInflater is not thread save as it maintains a global variable 'mConstructorArgs'.
-        // Create a different copy to use on the background thread.
-        LayoutInflater inflater = mInflater.cloneInContext(mInflater.getContext());
-
         // Inflate views on a non looper thread. This allows us to catch errors like calling
         // "new Handler()" in constructor easily.
         new Thread(() -> {
             for (int i = 0; i < initialSize; i++) {
-                T view = inflateNewView(inflater);
+                T view = inflateNewView();
                 handler.post(() -> addToPool(view));
             }
-        }, "ViewPool-init").start();
+        }).start();
     }
 
     @UiThread
@@ -98,12 +94,12 @@
             mCurrentSize--;
             return (T) mPool[mCurrentSize];
         }
-        return inflateNewView(mInflater);
+        return inflateNewView();
     }
 
     @AnyThread
-    private T inflateNewView(LayoutInflater inflater) {
-        return (T) inflater.inflate(mLayoutId, mParent, false);
+    private T inflateNewView() {
+        return (T) mInflater.inflate(mLayoutId, mParent, false);
     }
 
     /**
diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java
index 195a77a..a4518ba 100644
--- a/src/com/android/launcher3/views/AbstractSlideInView.java
+++ b/src/com/android/launcher3/views/AbstractSlideInView.java
@@ -32,14 +32,13 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.touch.BaseSwipeDetector;
-import com.android.launcher3.touch.SingleAxisSwipeDetector;
+import com.android.launcher3.touch.SwipeDetector;
 
 /**
  * Extension of AbstractFloatingView with common methods for sliding in from bottom
  */
 public abstract class AbstractSlideInView extends AbstractFloatingView
-        implements SingleAxisSwipeDetector.Listener {
+        implements SwipeDetector.Listener {
 
     protected static Property<AbstractSlideInView, Float> TRANSLATION_SHIFT =
             new Property<AbstractSlideInView, Float>(Float.class, "translationShift") {
@@ -58,7 +57,7 @@
     protected static final float TRANSLATION_SHIFT_OPENED = 0f;
 
     protected final Launcher mLauncher;
-    protected final SingleAxisSwipeDetector mSwipeDetector;
+    protected final SwipeDetector mSwipeDetector;
     protected final ObjectAnimator mOpenCloseAnimator;
 
     protected View mContent;
@@ -74,8 +73,7 @@
         mLauncher = Launcher.getLauncher(context);
 
         mScrollInterpolator = Interpolators.SCROLL_CUBIC;
-        mSwipeDetector = new SingleAxisSwipeDetector(context, this,
-                SingleAxisSwipeDetector.VERTICAL);
+        mSwipeDetector = new SwipeDetector(context, this, SwipeDetector.VERTICAL);
 
         mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this);
         mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
@@ -99,7 +97,7 @@
         }
 
         int directionsToDetectScroll = mSwipeDetector.isIdleState() ?
-                SingleAxisSwipeDetector.DIRECTION_NEGATIVE : 0;
+                SwipeDetector.DIRECTION_NEGATIVE : 0;
         mSwipeDetector.setDetectableScrollConditions(
                 directionsToDetectScroll, false);
         mSwipeDetector.onTouchEvent(ev);
@@ -124,7 +122,7 @@
         return mIsOpen && mOpenCloseAnimator.isRunning();
     }
 
-    /* SingleAxisSwipeDetector.Listener */
+    /* SwipeDetector.Listener */
 
     @Override
     public void onDragStart(boolean start) { }
@@ -138,17 +136,17 @@
     }
 
     @Override
-    public void onDragEnd(float velocity) {
-        if ((mSwipeDetector.isFling(velocity) && velocity > 0) || mTranslationShift > 0.5f) {
+    public void onDragEnd(float velocity, boolean fling) {
+        if ((fling && velocity > 0) || mTranslationShift > 0.5f) {
             mScrollInterpolator = scrollInterpolatorForVelocity(velocity);
-            mOpenCloseAnimator.setDuration(BaseSwipeDetector.calculateDuration(
+            mOpenCloseAnimator.setDuration(SwipeDetector.calculateDuration(
                     velocity, TRANSLATION_SHIFT_CLOSED - mTranslationShift));
             close(true);
         } else {
             mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat(
                     TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
             mOpenCloseAnimator.setDuration(
-                    BaseSwipeDetector.calculateDuration(velocity, mTranslationShift))
+                    SwipeDetector.calculateDuration(velocity, mTranslationShift))
                     .setInterpolator(Interpolators.DEACCEL);
             mOpenCloseAnimator.start();
         }
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index e43fc8a..2a4c5a7 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -170,8 +170,10 @@
             // Only look for controllers if we are not dispatching from gesture area and proxy is
             // not active
             mActiveController = findControllerToHandleTouch(ev);
+
+            if (mActiveController != null) return true;
         }
-        return mActiveController != null;
+        return false;
     }
 
     @Override
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index c63d745..45c0d90 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -415,7 +415,7 @@
             int width = isFolderIcon ? originalView.getWidth() : (int) pos.width();
             int height = isFolderIcon ? originalView.getHeight() : (int) pos.height();
             if (supportsAdaptiveIcons) {
-                drawable = getFullDrawable(l, info, width, height, sTmpObjArray);
+                drawable = getFullDrawable(l, info, width, height, false, sTmpObjArray);
                 if (drawable instanceof AdaptiveIconDrawable) {
                     badge = getBadge(l, info, sTmpObjArray[0]);
                 } else {
@@ -428,7 +428,7 @@
                     // Similar to DragView, we simply use the BubbleTextView icon here.
                     drawable = btvIcon;
                 } else {
-                    drawable = getFullDrawable(l, info, width, height, sTmpObjArray);
+                    drawable = getFullDrawable(l, info, width, height, false, sTmpObjArray);
                 }
             }
         }
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index 6038873..50db40f 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -16,9 +16,6 @@
 
 package com.android.launcher3.widget;
 
-import static com.android.launcher3.FastBitmapDrawable.newIcon;
-import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
-
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -36,11 +33,12 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.FastBitmapDrawable;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.ItemInfoWithIcon;
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.R;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
+import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.util.Themes;
@@ -130,22 +128,24 @@
             mCenterDrawable.setCallback(null);
             mCenterDrawable = null;
         }
-        if (info.bitmap.icon != null) {
+        if (info.iconBitmap != null) {
             // The view displays three modes,
             //   1) App icon in the center
             //   2) Preload icon in the center
             //   3) Setup icon in the center and app icon in the top right corner.
+            DrawableFactory drawableFactory = DrawableFactory.INSTANCE.get(getContext());
             if (mDisabledForSafeMode) {
-                FastBitmapDrawable disabledIcon = newIcon(getContext(), info);
+                FastBitmapDrawable disabledIcon = drawableFactory.newIcon(getContext(), info);
                 disabledIcon.setIsDisabled(true);
                 mCenterDrawable = disabledIcon;
                 mSettingIconDrawable = null;
             } else if (isReadyForClickSetup()) {
-                mCenterDrawable = newIcon(getContext(), info);
+                mCenterDrawable = drawableFactory.newIcon(getContext(), info);
                 mSettingIconDrawable = getResources().getDrawable(R.drawable.ic_setting).mutate();
-                updateSettingColor(info.bitmap.color);
+                updateSettingColor(info.iconColor);
             } else {
-                mCenterDrawable = newPendingIcon(getContext(), info);
+                mCenterDrawable = DrawableFactory.INSTANCE.get(getContext())
+                        .newPendingIcon(getContext(), info);
                 mSettingIconDrawable = null;
                 applyState();
             }
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index f713b33..6944879 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.SimpleOnStylusPressListener;
 import com.android.launcher3.StylusEventHelper;
 import com.android.launcher3.WidgetPreviewLoader;
+import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.icons.BaseIconFactory;
 import com.android.launcher3.model.WidgetItem;
 
@@ -181,8 +182,10 @@
             return;
         }
         if (bitmap != null) {
-            mWidgetImage.setBitmap(bitmap, mWidgetPreviewLoader.getBadgeForUser(mItem.user,
-                    BaseIconFactory.getBadgeSizeForIconSize(mDeviceProfile.allAppsIconSizePx)));
+            mWidgetImage.setBitmap(bitmap,
+                    DrawableFactory.INSTANCE.get(getContext()).getBadgeForUser(mItem.user,
+                            getContext(), BaseIconFactory.getBadgeSizeForIconSize(
+                                    mDeviceProfile.allAppsIconSizePx)));
             if (mAnimatePreview) {
                 mWidgetImage.setAlpha(0f);
                 ViewPropertyAnimator anim = mWidgetImage.animate();
diff --git a/src/com/android/launcher3/widget/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
index f3b325d..435125b 100644
--- a/src/com/android/launcher3/widget/WidgetsDiffReporter.java
+++ b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
@@ -18,8 +18,6 @@
 
 import android.util.Log;
 
-import androidx.recyclerview.widget.RecyclerView;
-
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.widget.WidgetsListAdapter.WidgetListRowEntryComparator;
@@ -27,6 +25,8 @@
 import java.util.ArrayList;
 import java.util.Iterator;
 
+import androidx.recyclerview.widget.RecyclerView;
+
 /**
  * Do diff on widget's tray list items and call the {@link RecyclerView.Adapter}
  * methods accordingly.
@@ -137,7 +137,7 @@
     }
 
     private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) {
-        return curInfo.bitmap.icon.equals(newInfo.bitmap.icon)
-                && !mIconCache.isDefaultIcon(curInfo.bitmap, curInfo.user);
+        return curInfo.iconBitmap.equals(newInfo.iconBitmap) &&
+                !mIconCache.isDefaultIcon(curInfo.iconBitmap, curInfo.user);
     }
 }
diff --git a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
deleted file mode 100644
index 60eb304..0000000
--- a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
+++ /dev/null
@@ -1,44 +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.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_plugins/com/android/systemui/plugins/RecentsExtraCard.java b/src_plugins/com/android/systemui/plugins/RecentsExtraCard.java
index cd9f33d..0ebea3d 100644
--- a/src_plugins/com/android/systemui/plugins/RecentsExtraCard.java
+++ b/src_plugins/com/android/systemui/plugins/RecentsExtraCard.java
@@ -34,9 +34,9 @@
     /**
      * Sets up the recents overview extra card and fills in data.
      *
-     * @param context     Plugin context
+     * @param context Plugin context
      * @param frameLayout PlaceholderView
-     * @param activity    Recents activity to hold extra view
+     * @param activity Recents activity to hold extra view
      */
     void setupView(Context context, FrameLayout frameLayout, Activity activity);
 }
diff --git a/src/com/android/launcher3/touch/AllAppsSwipeController.java b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
similarity index 75%
rename from src/com/android/launcher3/touch/AllAppsSwipeController.java
rename to src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
index 31a5d79..bd6ea50 100644
--- a/src/com/android/launcher3/touch/AllAppsSwipeController.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
@@ -1,19 +1,4 @@
-/**
- * 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;
+package com.android.launcher3.uioverrides;
 
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
@@ -24,6 +9,8 @@
 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;
 
 /**
@@ -34,7 +21,7 @@
     private MotionEvent mTouchDownEvent;
 
     public AllAppsSwipeController(Launcher l) {
-        super(l, SingleAxisSwipeDetector.VERTICAL);
+        super(l, SwipeDetector.VERTICAL);
     }
 
     @Override
@@ -71,8 +58,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_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
deleted file mode 100644
index 5407ea3..0000000
--- a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
+++ /dev/null
@@ -1,36 +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.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
new file mode 100644
index 0000000..6d9ed88
--- /dev/null
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
@@ -0,0 +1,105 @@
+/*
+ * 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) { }
+
+    /** No-op. */
+    public static void clearSwipeSharedState(Launcher launcher, boolean finishAnimation) { }
+
+    public static Person[] getPersons(ShortcutInfo si) {
+        return Utilities.EMPTY_PERSON_ARRAY;
+    }
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 5cf96c8..24b5b02 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -18,7 +18,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     package="com.android.launcher3.tests">
 
-    <uses-sdk android:targetSdkVersion="29" android:minSdkVersion="25"
+    <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="25"
               tools:overrideLibrary="android.support.test.uiautomator.v18"/>
 
     <application android:debuggable="true">
diff --git a/tests/dummy_app/AndroidManifest.xml b/tests/dummy_app/AndroidManifest.xml
index f00138c..9d0a74a 100644
--- a/tests/dummy_app/AndroidManifest.xml
+++ b/tests/dummy_app/AndroidManifest.xml
@@ -21,7 +21,7 @@
      to come from a domain that you own or have control over. -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.example.android.aardwolf">
-    <uses-sdk android:targetSdkVersion="29" android:minSdkVersion="21"/>
+    <uses-sdk android:targetSdkVersion="28" android:minSdkVersion="21"/>
     <application android:label="Aardwolf">
         <activity
             android:name="Activity1"
diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 0dcfaa8..7029ad5 100644
--- a/tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -142,7 +142,7 @@
         when(mMockIconCache.getDefaultIcon(eq(mLoaderCursor.user)))
                 .thenReturn(BitmapInfo.fromBitmap(icon));
         WorkspaceItemInfo info = mLoaderCursor.loadSimpleWorkspaceItem();
-        assertEquals(icon, info.bitmap.icon);
+        assertEquals(icon, info.iconBitmap);
         assertEquals("my-shortcut", info.title);
         assertEquals(ITEM_TYPE_SHORTCUT, info.itemType);
     }
diff --git a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java b/tests/src/com/android/launcher3/touch/SwipeDetectorTest.java
similarity index 72%
rename from tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
rename to tests/src/com/android/launcher3/touch/SwipeDetectorTest.java
index 5174e4d..f209fae 100644
--- a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
+++ b/tests/src/com/android/launcher3/touch/SwipeDetectorTest.java
@@ -15,12 +15,6 @@
  */
 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;
@@ -45,19 +39,19 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
-public class SingleAxisSwipeDetectorTest {
+public class SwipeDetectorTest {
 
-    private static final String TAG = SingleAxisSwipeDetectorTest.class.getSimpleName();
+    private static final String TAG = SwipeDetectorTest.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 SingleAxisSwipeDetector mDetector;
+    private SwipeDetector mDetector;
     private int mTouchSlop;
 
     @Mock
-    private SingleAxisSwipeDetector.Listener mMockListener;
+    private SwipeDetector.Listener mMockListener;
 
     @Mock
     private ViewConfiguration mMockConfig;
@@ -71,8 +65,8 @@
         doReturn(orgConfig.getScaledMaximumFlingVelocity()).when(mMockConfig)
                 .getScaledMaximumFlingVelocity();
 
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false);
-        mDetector.setDetectableScrollConditions(DIRECTION_BOTH, false);
+        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.VERTICAL, false);
+        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, false);
         mTouchSlop = orgConfig.getScaledTouchSlop();
         doReturn(mTouchSlop).when(mMockConfig).getScaledTouchSlop();
 
@@ -81,8 +75,8 @@
 
     @Test
     public void testDragStart_verticalPositive() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false);
-        mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
+        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.VERTICAL, false);
+        mDetector.setDetectableScrollConditions(SwipeDetector.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.
@@ -91,8 +85,8 @@
 
     @Test
     public void testDragStart_verticalNegative() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, VERTICAL, false);
-        mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
+        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.VERTICAL, false);
+        mDetector.setDetectableScrollConditions(SwipeDetector.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.
@@ -109,8 +103,8 @@
 
     @Test
     public void testDragStart_horizontalPositive() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, false);
-        mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
+        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, false);
+        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
 
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 + mTouchSlop, 100);
@@ -120,8 +114,8 @@
 
     @Test
     public void testDragStart_horizontalNegative() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, false);
-        mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
+        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, false);
+        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_NEGATIVE, false);
 
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 - mTouchSlop, 100);
@@ -131,8 +125,8 @@
 
     @Test
     public void testDragStart_horizontalRtlPositive() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, true);
-        mDetector.setDetectableScrollConditions(DIRECTION_POSITIVE, false);
+        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, true);
+        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
 
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 - mTouchSlop, 100);
@@ -142,8 +136,8 @@
 
     @Test
     public void testDragStart_horizontalRtlNegative() {
-        mDetector = new SingleAxisSwipeDetector(mMockConfig, mMockListener, HORIZONTAL, true);
-        mDetector.setDetectableScrollConditions(DIRECTION_NEGATIVE, false);
+        mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, true);
+        mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_NEGATIVE, false);
 
         mGenerator.put(0, 100, 100);
         mGenerator.move(0, 100 + mTouchSlop, 100);
@@ -166,6 +160,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());
+        verify(mMockListener).onDragEnd(anyFloat(), anyBoolean());
     }
 }
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index bb19515..62989a3 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -304,7 +304,7 @@
     protected void waitForLauncherCondition(
             String message, Function<Launcher, Boolean> condition, long timeout) {
         if (!TestHelpers.isInLauncherProcess()) return;
-        Wait.atMost(message, () -> getFromLauncher(condition), timeout, mLauncher);
+        Wait.atMost(message, () -> getFromLauncher(condition), timeout);
     }
 
     // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
@@ -317,7 +317,7 @@
             final Object fromLauncher = getFromLauncher(f);
             output[0] = fromLauncher;
             return fromLauncher != null;
-        }, timeout, mLauncher);
+        }, timeout);
         return (T) output[0];
     }
 
@@ -331,7 +331,7 @@
         Wait.atMost(message, () -> {
             testThreadAction.run();
             return getFromLauncher(condition);
-        }, timeout, mLauncher);
+        }, timeout);
     }
 
     protected LauncherActivityInfo getSettingsApp() {
@@ -373,8 +373,7 @@
         startIntent(
                 getInstrumentation().getContext().getPackageManager().getLaunchIntentForPackage(
                         packageName),
-                By.pkg(packageName).depth(0),
-                true /* newTask */);
+                By.pkg(packageName).depth(0));
     }
 
     public static void startTestActivity(int activityNumber) {
@@ -383,17 +382,12 @@
                 getLaunchIntentForPackage(packageName);
         intent.setComponent(new ComponentName(packageName,
                 "com.android.launcher3.tests.Activity" + activityNumber));
-        startIntent(intent, By.pkg(packageName).text("TestActivity" + activityNumber),
-                false /* newTask */);
+        startIntent(intent, By.pkg(packageName).text("TestActivity" + activityNumber));
     }
 
-    private static void startIntent(Intent intent, BySelector selector, boolean newTask) {
+    private static void startIntent(Intent intent, BySelector selector) {
         intent.addCategory(Intent.CATEGORY_LAUNCHER);
-        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);
-        }
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
         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 0321bcd..5e87612 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -18,10 +18,6 @@
 
 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;
@@ -38,7 +34,6 @@
 import com.android.launcher3.tapl.AppIcon;
 import com.android.launcher3.tapl.AppIconMenu;
 import com.android.launcher3.tapl.AppIconMenuItem;
-import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.tapl.Widgets;
 import com.android.launcher3.tapl.Workspace;
 import com.android.launcher3.views.OptionsPopupView;
@@ -354,7 +349,6 @@
     @Ignore("Temporarily disabled to unblock merging to master")
     @PortraitLandscape
     public void testDragCustomShortcut() {
-        if (!TestHelpers.isInLauncherProcess()) return;     // b/143725213
         mLauncher.getWorkspace().openAllWidgets()
                 .getWidget("com.android.launcher3.testcomponent.CustomShortcutConfigActivity")
                 .dragToWorkspace();
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index 0472ce1..e1b3ede 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -103,12 +103,12 @@
 
         setResult(acceptConfig);
         if (acceptConfig) {
-            Wait.atMost(null, new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
+            Wait.atMost(null, new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT);
             assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
         } else {
             // Verify that the widget id is deleted.
             Wait.atMost(null, () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
-                    DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
+                    DEFAULT_ACTIVITY_TIMEOUT);
         }
     }
 
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index 259f9ed..b8ca5de 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -16,7 +16,6 @@
 package com.android.launcher3.ui.widget;
 
 import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
-import static com.android.launcher3.util.rule.TestStabilityRule.UNBUNDLED_POSTSUBMIT;
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -30,7 +29,6 @@
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.rule.ShellCommandRule;
-import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 
 import org.junit.Rule;
 import org.junit.Test;
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index d909ad7..07129dd 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -59,8 +59,7 @@
 @RunWith(AndroidJUnit4.class)
 public class RequestPinItemTest extends AbstractLauncherUiTest {
 
-    @Rule
-    public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
+    @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
 
     private String mCallbackAction;
     private String mShortcutId;
@@ -85,10 +84,10 @@
                         .equals(AppWidgetNoConfig.class.getName()));
     }
 
-    @Test
+        @Test
     public void testPinWidgetNoConfig_customPreview() throws Throwable {
         // Command to set custom preview
-        Intent command = RequestPinItemActivity.getCommandIntent(
+        Intent command =  RequestPinItemActivity.getCommandIntent(
                 RequestPinItemActivity.class, "setRemoteViewColor").putExtra(
                 RequestPinItemActivity.EXTRA_PARAM + "0", Color.RED);
 
@@ -170,8 +169,7 @@
 
         // Go back to home
         mLauncher.pressHome();
-        Wait.atMost(null, new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT,
-                mLauncher);
+        Wait.atMost(null, new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT);
     }
 
     /**
diff --git a/tests/src/com/android/launcher3/util/Wait.java b/tests/src/com/android/launcher3/util/Wait.java
index 2663d02..899686b 100644
--- a/tests/src/com/android/launcher3/util/Wait.java
+++ b/tests/src/com/android/launcher3/util/Wait.java
@@ -3,8 +3,6 @@
 import android.os.SystemClock;
 import android.util.Log;
 
-import com.android.launcher3.tapl.LauncherInstrumentation;
-
 import org.junit.Assert;
 
 /**
@@ -14,13 +12,11 @@
 
     private static final long DEFAULT_SLEEP_MS = 200;
 
-    public static void atMost(String message, Condition condition, long timeout,
-            LauncherInstrumentation launcher) {
-        atMost(message, condition, timeout, DEFAULT_SLEEP_MS, launcher);
+    public static void atMost(String message, Condition condition, long timeout) {
+        atMost(message, condition, timeout, DEFAULT_SLEEP_MS);
     }
 
-    public static void atMost(String message, Condition condition, long timeout, long sleepMillis,
-            LauncherInstrumentation launcher) {
+    public static void atMost(String message, Condition condition, long timeout, long sleepMillis) {
         final long startTime = SystemClock.uptimeMillis();
         long endTime = startTime + timeout;
         Log.d("Wait", "atMost: " + startTime + " - " + endTime);
@@ -44,7 +40,6 @@
             throw new RuntimeException(t);
         }
         Log.d("Wait", "atMost: timed out: " + SystemClock.uptimeMillis());
-        launcher.checkForAnomaly();
         Assert.fail(message);
     }
 }
diff --git a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
index 858cb38..69bf01d 100644
--- a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
@@ -21,7 +21,6 @@
 import android.os.Build;
 import android.util.Log;
 
-import androidx.test.InstrumentationRegistry;
 import androidx.test.uiautomator.UiDevice;
 
 import org.junit.rules.TestRule;
@@ -58,7 +57,7 @@
     public static final int PLATFORM_PRESUBMIT = 0x8;
     public static final int PLATFORM_POSTSUBMIT = 0x10;
 
-    public static final int RUN_FLAFOR = getRunFlavor();
+    private static final int RUN_FLAFOR = getRunFlavor();
 
     @Retention(RetentionPolicy.RUNTIME)
     @Target(ElementType.METHOD)
@@ -87,19 +86,6 @@
     }
 
     private static int getRunFlavor() {
-        final String flavorOverride = InstrumentationRegistry.getArguments().getString("flavor");
-
-        if (flavorOverride != null) {
-            Log.d(TAG, "Flavor override: " + flavorOverride);
-            try {
-                return (int) TestStabilityRule.class.getField(flavorOverride).get(null);
-            } catch (NoSuchFieldException e) {
-                throw new AssertionError("Unrecognized run flavor override: " + flavorOverride);
-            } catch (IllegalAccessException e) {
-                throw new RuntimeException(e);
-            }
-        }
-
         final String launcherVersion;
         try {
             launcherVersion = getInstrumentation().
diff --git a/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java b/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
index c7f7cd6..a31d8a6 100644
--- a/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
+++ b/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
@@ -23,19 +23,16 @@
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
 import android.graphics.Bitmap;
-import android.view.LayoutInflater;
-
-import androidx.recyclerview.widget.RecyclerView;
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
+import android.view.LayoutInflater;
 
+import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.WidgetPreviewLoader;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.util.MultiHashMap;
@@ -49,6 +46,8 @@
 import java.util.ArrayList;
 import java.util.Map;
 
+import androidx.recyclerview.widget.RecyclerView;
+
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class WidgetsListAdapterTest {
@@ -137,7 +136,7 @@
             PackageItemInfo pInfo = new PackageItemInfo(wi.componentName.getPackageName());
             pInfo.title = pInfo.packageName;
             pInfo.user = wi.user;
-            pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+            pInfo.iconBitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8);
             newMap.addToList(pInfo, wi);
             if (newMap.size() == num) {
                 break;
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 1ecfff7..e1e9b8d 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -29,8 +29,6 @@
 import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.testing.TestProtocol;
 
-import java.util.stream.Collectors;
-
 /**
  * Operations on AllApps opened from Home. Also a parent for All Apps opened from Overview.
  */
@@ -69,7 +67,7 @@
             return false;
         }
         if (iconBounds.bottom > displayBottom) {
-            LauncherInstrumentation.log("hasClickableIcon: icon bottom below bottom offset");
+            LauncherInstrumentation.log("hasClickableIcon: icon center bellow bottom offset");
             return false;
         }
         LauncherInstrumentation.log("hasClickableIcon: icon is clickable");
@@ -118,12 +116,7 @@
                             displayBottom)) {
                         mLauncher.scrollToLastVisibleRow(
                                 allAppsContainer,
-                                mLauncher.getObjectsInContainer(allAppsContainer, "icon")
-                                        .stream()
-                                        .filter(object ->
-                                                object.getVisibleBounds().bottom
-                                                        <= displayBottom)
-                                        .collect(Collectors.toList()),
+                                mLauncher.getObjectsInContainer(allAppsContainer, "icon"),
                                 searchBox.getVisibleBounds().bottom
                                         - allAppsContainer.getVisibleBounds().top);
                         final int newScroll = getAllAppsScroll();
@@ -170,7 +163,7 @@
                         "Exceeded max scroll attempts: " + MAX_SCROLL_ATTEMPTS,
                         ++attempts <= MAX_SCROLL_ATTEMPTS);
 
-                mLauncher.scroll(allAppsContainer, Direction.UP, margins, 12);
+                mLauncher.scroll(allAppsContainer, Direction.UP, margins, 50);
             }
 
             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 2650cf9..0879404 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -64,12 +64,10 @@
 import com.android.launcher3.testing.TestProtocol;
 import com.android.systemui.shared.system.QuickStepContract;
 
-import org.junit.Assert;
-
+import java.util.ArrayList;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.lang.ref.WeakReference;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Deque;
@@ -79,6 +77,8 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 
+import org.junit.Assert;
+
 /**
  * The main tapl object. The only object that can be explicitly constructed by the using code. It
  * produces all other objects.
@@ -298,14 +298,6 @@
         return null;
     }
 
-    public void checkForAnomaly() {
-        final String anomalyMessage = getAnomalyMessage();
-        if (anomalyMessage != null) {
-            failWithSystemHealth(
-                    "Tests are broken by a non-Launcher system error: " + anomalyMessage);
-        }
-    }
-
     private String getVisibleStateMessage() {
         if (hasLauncherObject(WIDGETS_RES_ID)) return "Widgets";
         if (hasLauncherObject(OVERVIEW_RES_ID)) return "Overview";
@@ -339,17 +331,20 @@
     }
 
     private void fail(String message) {
-        checkForAnomaly();
+        message = "http://go/tapl : " + getContextDescription() + message;
 
-        failWithSystemHealth("http://go/tapl : " + getContextDescription() + message +
-                " (visible state: " + getVisibleStateMessage() + ")");
-    }
+        final String anomaly = getAnomalyMessage();
+        if (anomaly != null) {
+            message = anomaly + ", which causes:\n" + message;
+        } else {
+            message = message + " (visible state: " + getVisibleStateMessage() + ")";
+        }
 
-    private void failWithSystemHealth(String message) {
         final String systemHealth = getSystemHealthMessage();
         if (systemHealth != null) {
             message = message
-                    + ", perhaps because of system health problems:\n<<<<<<<<<<<<<<<<<<\n"
+                    + ", which might be a consequence of system health "
+                    + "problems:\n<<<<<<<<<<<<<<<<<<\n"
                     + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
         }
 
@@ -429,7 +424,11 @@
         // b/136278866
         for (int i = 0; i != 100; ++i) {
             if (getNavigationModeMismatchError() == null) break;
-            sleep(100);
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
         }
 
         final String error = getNavigationModeMismatchError();
@@ -536,7 +535,8 @@
         // accessibility events prior to pressing Home.
         final String action;
         if (getNavigationModel() == NavigationModel.ZERO_BUTTON) {
-            checkForAnomaly();
+            final String anomaly = getAnomalyMessage();
+            if (anomaly != null) fail("Can't swipe up to Home: " + anomaly);
 
             final Point displaySize = getRealDisplaySize();
 
@@ -795,18 +795,15 @@
         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
-        final int totalMargin = Math.max(bottomMargin, getBottomGestureMargin(container));
         scroll(
                 container,
                 Direction.DOWN,
                 new Rect(
                         0,
-                        totalMargin / 2,
                         0,
-                        totalMargin / 2),
-                80);
+                        0,
+                        Math.max(bottomMargin, getBottomGestureMargin(container))),
+                150);
     }
 
     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 16a64a7..4f8aeb1 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,
-                    12,
+                    50,
                     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 db3314e..d1261e0 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_DURATION = 500;
+    private static final int DRAG_DURACTION = 2000;
     private static final int FLING_STEPS = 10;
     private final UiObject2 mHotseat;
 
@@ -72,7 +72,7 @@
                     start.y,
                     start.x,
                     start.y - swipeHeight - mLauncher.getTouchSlop(),
-                    12,
+                    60,
                     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_DURATION, launchableCenter, dest);
+                downTime, SystemClock.uptimeMillis(), DRAG_DURACTION, launchableCenter, dest);
         LauncherInstrumentation.log("dragIconToWorkspace: moved pointer");
         launcher.sendPointer(
                 downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest);