Merge "Prevent all apps prediction update while visible" into ub-launcher3-rvc-dev
diff --git a/protos/launcher_atom.proto b/protos/launcher_atom.proto
index cac2d8f..f1b71e8 100644
--- a/protos/launcher_atom.proto
+++ b/protos/launcher_atom.proto
@@ -95,7 +95,18 @@
 
 // Represents folder in a closed state.
 message FolderIcon {
+  // Number of items inside folder.
   optional int32 cardinality = 1;
+
+  // State of the folder label before the event.
+  optional FromState from_label_state = 2;
+
+  // State of the folder label after the event.
+  optional ToState to_label_state = 3;
+
+  // Details about actual folder label.
+  // Populated when folder label is not a PII.
+  optional string label_info = 4;
 }
 
 //////////////////////////////////////////////
@@ -120,3 +131,78 @@
     HotseatContainer hotseat = 5;
   }
 }
+
+// Represents state of EditText field before update.
+enum FromState {
+  // Default value.
+  // Used when a FromState is not applicable, for example, during folder creation.
+  FROM_STATE_UNSPECIFIED = 0;
+
+  // EditText was empty.
+  // Eg: When a folder label is updated from empty string.
+  FROM_EMPTY = 1;
+
+  // EditText was non-empty and manually entered by the user.
+  // Eg: When a folder label is updated from a user-entered value.
+  FROM_CUSTOM = 2;
+
+  // EditText was non-empty and one of the suggestions.
+  // Eg: When a folder label is updated from a suggested value.
+  FROM_SUGGESTED = 3;
+}
+
+// Represents state of EditText field after update.
+enum ToState {
+  // Default value.
+  // Used when ToState is not applicable, for example, when folder label is updated to a different
+  // value when folder label suggestion feature is disabled.
+  TO_STATE_UNSPECIFIED = 0;
+
+  // User attempted to change the EditText, but was not changed.
+  UNCHANGED = 1;
+
+  // New label matches with primary(aka top) suggestion.
+  TO_SUGGESTION0 = 2;
+
+  // New value matches with second top suggestion even though the top suggestion was non-empty.
+  TO_SUGGESTION1_WITH_VALID_PRIMARY = 3;
+
+  // New value matches with second top suggestion given that top suggestion was empty.
+  TO_SUGGESTION1_WITH_EMPTY_PRIMARY = 4;
+
+  // New value matches with third top suggestion even though the top suggestion was non-empty.
+  TO_SUGGESTION2_WITH_VALID_PRIMARY = 5;
+
+  // New value matches with third top suggestion given that top suggestion was empty.
+  TO_SUGGESTION2_WITH_EMPTY_PRIMARY = 6;
+
+  // New value matches with 4th top suggestion even though the top suggestion was non-empty.
+  TO_SUGGESTION3_WITH_VALID_PRIMARY = 7;
+
+  // New value matches with 4th top suggestion given that top suggestion was empty.
+  TO_SUGGESTION3_WITH_EMPTY_PRIMARY = 8;
+
+  // New value is empty even though the top suggestion was non-empty.
+  TO_EMPTY_WITH_VALID_PRIMARY = 9;
+
+  // New value is empty given that top suggestion was empty.
+  TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY = 10;
+
+  // New value is empty given that no suggestions were provided.
+  TO_EMPTY_WITH_EMPTY_SUGGESTIONS = 11;
+
+  // New value is empty given that suggestions feature was disabled.
+  TO_EMPTY_WITH_SUGGESTIONS_DISABLED = 12;
+
+  // New value is non-empty and does not match with any of the suggestions even though the top suggestion was non-empty.
+  TO_CUSTOM_WITH_VALID_PRIMARY = 13;
+
+  // New value is non-empty and not match with any suggestions given that top suggestion was empty.
+  TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY = 14;
+
+  // New value is non-empty and also no suggestions were provided.
+  TO_CUSTOM_WITH_EMPTY_SUGGESTIONS = 15;
+
+  // New value is non-empty and also suggestions feature was disable.
+  TO_CUSTOM_WITH_SUGGESTIONS_DISABLED = 16;
+}
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 04506b5..b2286f1 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -95,12 +95,6 @@
             android:clearTaskOnLaunch="true"
             android:exported="false" />
 
-        <activity android:name="com.android.quickstep.LockScreenRecentsActivity"
-                  android:theme="@android:style/Theme.NoDisplay"
-                  android:showOnLockScreen="true"
-                  android:taskAffinity="${packageName}.locktask"
-                  android:directBootAware="true" />
-
         <activity
             android:name="com.android.quickstep.interaction.GestureSandboxActivity"
             android:autoRemoveFromRecents="true"
diff --git a/quickstep/recents_ui_overrides/res/values/override.xml b/quickstep/recents_ui_overrides/res/values/override.xml
index ed3ba92..6aa9619 100644
--- a/quickstep/recents_ui_overrides/res/values/override.xml
+++ b/quickstep/recents_ui_overrides/res/values/override.xml
@@ -26,5 +26,7 @@
   <string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string>
 
   <string name="user_event_dispatcher_class" translatable="false">com.android.quickstep.logging.UserEventDispatcherAppPredictionExtension</string>
+
+  <string name="prediction_model_class" translatable="false">com.android.launcher3.hybridhotseat.HotseatPredictionModel</string>
 </resources>
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
index 7f8f0a0..e4d0adf 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
@@ -121,7 +121,7 @@
         if (!putIntoFolder.isEmpty()) {
             ItemInfo firstItem = putIntoFolder.get(0);
             FolderInfo folderInfo = new FolderInfo();
-            folderInfo.title = "";
+            folderInfo.setTitle("");
             mLauncher.getModelWriter().addItemToDatabase(folderInfo, firstItem.container,
                     firstItem.screenId, firstItem.cellX, firstItem.cellY);
             folderInfo.contents.addAll(putIntoFolder);
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index e9f3534..7a73e50 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -25,10 +25,7 @@
 import android.app.prediction.AppPredictor;
 import android.app.prediction.AppTarget;
 import android.app.prediction.AppTargetEvent;
-import android.app.prediction.AppTargetId;
 import android.content.ComponentName;
-import android.os.Bundle;
-import android.os.Process;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
@@ -36,8 +33,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.CellLayout;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.Hotseat;
@@ -48,7 +43,6 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.Workspace;
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.appprediction.ComponentKeyMapper;
@@ -57,12 +51,10 @@
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.model.PredictionModel;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
-import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.shortcuts.ShortcutKey;
@@ -77,7 +69,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Locale;
 import java.util.OptionalInt;
 import java.util.stream.IntStream;
 
@@ -93,17 +84,6 @@
     private static final String TAG = "PredictiveHotseat";
     private static final boolean DEBUG = false;
 
-    //TODO: replace this with AppTargetEvent.ACTION_UNPIN (b/144119543)
-    private static final int APPTARGET_ACTION_UNPIN = 4;
-
-    private static final String APP_LOCATION_HOTSEAT = "hotseat";
-    private static final String APP_LOCATION_WORKSPACE = "workspace";
-
-    private static final String BUNDLE_KEY_HOTSEAT = "hotseat_apps";
-    private static final String BUNDLE_KEY_WORKSPACE = "workspace_apps";
-
-    private static final String BUNDLE_KEY_PIN_EVENTS = "pin_events";
-
     private static final String PREDICTION_CLIENT = "hotseat";
     private DropTarget.DragObject mDragObject;
     private int mHotSeatItemsCount;
@@ -116,13 +96,14 @@
 
     private DynamicItemCache mDynamicItemCache;
 
-    private final PredictionModel mPredictionModel;
+    private final HotseatPredictionModel mPredictionModel;
     private AppPredictor mAppPredictor;
     private AllAppsStore mAllAppsStore;
     private AnimatorSet mIconRemoveAnimators;
     private boolean mUIUpdatePaused = false;
     private boolean mRequiresCacheUpdate = true;
     private boolean mIsCacheEmpty;
+    private boolean mIsDestroyed = false;
 
     private HotseatEduController mHotseatEduController;
 
@@ -141,13 +122,13 @@
         mLauncher = launcher;
         mHotseat = launcher.getHotseat();
         mAllAppsStore = mLauncher.getAppsView().getAppsStore();
-        mPredictionModel = LauncherAppState.INSTANCE.get(launcher).getPredictionModel();
+        LauncherAppState appState = LauncherAppState.getInstance(launcher);
+        mPredictionModel = (HotseatPredictionModel) appState.getPredictionModel();
         mAllAppsStore.addUpdateListener(this);
         mDynamicItemCache = new DynamicItemCache(mLauncher, this::fillGapsWithPrediction);
         mHotSeatItemsCount = mLauncher.getDeviceProfile().inv.numHotseatIcons;
         launcher.getDeviceProfile().inv.addOnChangeListener(this);
         mHotseat.addOnAttachStateChangeListener(this);
-        mIsCacheEmpty = mPredictionModel.getPredictionComponentKeys().isEmpty();
         if (mHotseat.isAttachedToWindow()) {
             onViewAttachedToWindow(mHotseat);
         }
@@ -260,6 +241,7 @@
      * Unregisters callbacks and frees resources
      */
     public void destroy() {
+        mIsDestroyed = true;
         mAllAppsStore.removeUpdateListener(this);
         mLauncher.getDeviceProfile().inv.removeOnChangeListener(this);
         mHotseat.removeOnAttachStateChangeListener(this);
@@ -293,99 +275,52 @@
         if (mAppPredictor != null) {
             mAppPredictor.destroy();
         }
-        mAppPredictor = apm.createAppPredictionSession(
-                new AppPredictionContext.Builder(mLauncher)
-                        .setUiSurface(PREDICTION_CLIENT)
-                        .setPredictedTargetCount(mHotSeatItemsCount)
-                        .setExtras(getAppPredictionContextExtra())
-                        .build());
         WeakReference<HotseatPredictionController> controllerRef = new WeakReference<>(this);
-        mAppPredictor.registerPredictionUpdates(mLauncher.getApplicationContext().getMainExecutor(),
-                list -> {
-                    if (controllerRef.get() != null) {
-                        controllerRef.get().setPredictedApps(list);
-                    }
-                });
 
+
+        mPredictionModel.createBundle(bundle -> {
+            if (mIsDestroyed) return;
+            mAppPredictor = apm.createAppPredictionSession(
+                    new AppPredictionContext.Builder(mLauncher)
+                            .setUiSurface(PREDICTION_CLIENT)
+                            .setPredictedTargetCount(mHotSeatItemsCount)
+                            .setExtras(bundle)
+                            .build());
+            mAppPredictor.registerPredictionUpdates(
+                    mLauncher.getApplicationContext().getMainExecutor(),
+                    list -> {
+                        if (controllerRef.get() != null) {
+                            controllerRef.get().setPredictedApps(list);
+                        }
+                    });
+            mAppPredictor.requestPredictionUpdate();
+        });
         setPauseUIUpdate(false);
         if (!isEduSeen()) {
             mHotseatEduController = new HotseatEduController(mLauncher, this::createPredictor);
         }
-        mAppPredictor.requestPredictionUpdate();
     }
 
     /**
      * Create WorkspaceItemInfo objects and binds PredictedAppIcon views for cached predicted items.
      */
-    public void showCachedItems(List<AppInfo> apps, IntArray ranks) {
+    public void showCachedItems(List<AppInfo> apps,  IntArray ranks) {
+        mIsCacheEmpty = apps.isEmpty();
         int count = Math.min(ranks.size(), apps.size());
         List<WorkspaceItemInfo> items = new ArrayList<>(count);
+        mComponentKeyMappers.clear();
         for (int i = 0; i < count; i++) {
             WorkspaceItemInfo item = new WorkspaceItemInfo(apps.get(i));
+            ComponentKey componentKey = new ComponentKey(item.getTargetComponent(), item.user);
             preparePredictionInfo(item, ranks.get(i));
             items.add(item);
-        }
-        mComponentKeyMappers.clear();
-        for (ComponentKey key : mPredictionModel.getPredictionComponentKeys()) {
-            mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache));
+
+            mComponentKeyMappers.add(new ComponentKeyMapper(componentKey, mDynamicItemCache));
         }
         updateDependencies();
         bindItems(items, false, null);
     }
 
-    private Bundle getAppPredictionContextExtra() {
-        Bundle bundle = new Bundle();
-
-        //TODO: remove this way of reporting items
-        bundle.putParcelableArrayList(BUNDLE_KEY_HOTSEAT,
-                getPinnedAppTargetsInViewGroup((mHotseat.getShortcutsAndWidgets())));
-        bundle.putParcelableArrayList(BUNDLE_KEY_WORKSPACE, getPinnedAppTargetsInViewGroup(
-                mLauncher.getWorkspace().getScreenWithId(
-                        Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets()));
-
-        ArrayList<AppTargetEvent> pinEvents = new ArrayList<>();
-        getPinEventsForViewGroup(pinEvents, mHotseat.getShortcutsAndWidgets(),
-                APP_LOCATION_HOTSEAT);
-        getPinEventsForViewGroup(pinEvents, mLauncher.getWorkspace().getScreenWithId(
-                Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets(), APP_LOCATION_WORKSPACE);
-        bundle.putParcelableArrayList(BUNDLE_KEY_PIN_EVENTS, pinEvents);
-
-        return bundle;
-    }
-
-    private ArrayList<AppTargetEvent> getPinEventsForViewGroup(ArrayList<AppTargetEvent> pinEvents,
-            ViewGroup views, String root) {
-        for (int i = 0; i < views.getChildCount(); i++) {
-            View child = views.getChildAt(i);
-            final AppTargetEvent event;
-            if (child.getTag() instanceof ItemInfo && getAppTargetFromInfo(
-                    (ItemInfo) child.getTag()) != null) {
-                ItemInfo info = (ItemInfo) child.getTag();
-                event = wrapAppTargetWithLocation(getAppTargetFromInfo(info),
-                        AppTargetEvent.ACTION_PIN, info);
-            } else {
-                CellLayout.LayoutParams params = (CellLayout.LayoutParams) views.getLayoutParams();
-                event = wrapAppTargetWithLocation(getBlockAppTarget(), AppTargetEvent.ACTION_PIN,
-                        root, 0, params.cellX, params.cellY, params.cellHSpan, params.cellVSpan);
-            }
-            pinEvents.add(event);
-        }
-        return pinEvents;
-    }
-
-
-    private ArrayList<AppTarget> getPinnedAppTargetsInViewGroup(ViewGroup viewGroup) {
-        ArrayList<AppTarget> pinnedApps = new ArrayList<>();
-        for (int i = 0; i < viewGroup.getChildCount(); i++) {
-            View child = viewGroup.getChildAt(i);
-            if (isPinnedIcon(child)) {
-                WorkspaceItemInfo itemInfo = (WorkspaceItemInfo) child.getTag();
-                pinnedApps.add(getAppTargetFromItemInfo(itemInfo));
-            }
-        }
-        return pinnedApps;
-    }
-
     private void setPredictedApps(List<AppTarget> appTargets) {
         mComponentKeyMappers.clear();
         StringBuilder predictionLog = new StringBuilder("predictedApps: [\n");
@@ -443,8 +378,11 @@
                 workspaceItemInfo.cellX, workspaceItemInfo.cellY);
         ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 1, 0.8f, 1).start();
         icon.pin(workspaceItemInfo);
-        AppTarget appTarget = getAppTargetFromItemInfo(workspaceItemInfo);
-        notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, AppTargetEvent.ACTION_PIN);
+        AppTarget appTarget = mPredictionModel.getAppTargetFromInfo(workspaceItemInfo);
+        if (appTarget != null) {
+            notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(appTarget,
+                    AppTargetEvent.ACTION_PIN, workspaceItemInfo));
+        }
         mRequiresCacheUpdate = true;
     }
 
@@ -524,10 +462,9 @@
         mIconRemoveAnimators.start();
     }
 
-    private void notifyItemAction(AppTarget target, String location, int action) {
+    private void notifyItemAction(AppTargetEvent event) {
         if (mAppPredictor != null) {
-            mAppPredictor.notifyAppTargetEvent(new AppTargetEvent.Builder(target,
-                    action).setLaunchLocation(location).build());
+            mAppPredictor.notifyAppTargetEvent(event);
         }
     }
 
@@ -545,36 +482,35 @@
     /**
      * Unpins pinned app when it's converted into a folder
      */
-    public void folderCreatedFromWorkspaceItem(ItemInfo info, FolderInfo folderInfo) {
-        if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
-            return;
+    public void folderCreatedFromWorkspaceItem(ItemInfo itemInfo, FolderInfo folderInfo) {
+        AppTarget folderTarget = mPredictionModel.getAppTargetFromInfo(folderInfo);
+        AppTarget itemTarget = mPredictionModel.getAppTargetFromInfo(itemInfo);
+        if (folderTarget != null && HotseatPredictionModel.isTrackedForPrediction(folderInfo)) {
+            notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(folderTarget,
+                    AppTargetEvent.ACTION_PIN, folderInfo));
         }
-        AppTarget target = getAppTargetFromItemInfo(info);
-        ViewGroup hotseatVG = mHotseat.getShortcutsAndWidgets();
-        ViewGroup firstScreenVG = mLauncher.getWorkspace().getScreenWithId(
-                Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets();
-
-        if (isInHotseat(folderInfo) && !getPinnedAppTargetsInViewGroup(hotseatVG).contains(
-                target)) {
-            notifyItemAction(target, APP_LOCATION_HOTSEAT, APPTARGET_ACTION_UNPIN);
-        } else if (isInFirstPage(folderInfo) && !getPinnedAppTargetsInViewGroup(
-                firstScreenVG).contains(target)) {
-            notifyItemAction(target, APP_LOCATION_WORKSPACE, APPTARGET_ACTION_UNPIN);
+        // using folder info with isTrackedForPrediction as itemInfo.container is already changed
+        // to folder by this point
+        if (itemTarget != null && HotseatPredictionModel.isTrackedForPrediction(folderInfo)) {
+            notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(itemTarget,
+                    AppTargetEvent.ACTION_UNPIN, folderInfo
+            ));
         }
     }
 
     /**
      * Pins workspace item created when all folder items are removed but one
      */
-    public void folderConvertedToWorkspaceItem(ItemInfo info, FolderInfo folderInfo) {
-        if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
-            return;
+    public void folderConvertedToWorkspaceItem(ItemInfo itemInfo, FolderInfo folderInfo) {
+        AppTarget folderTarget = mPredictionModel.getAppTargetFromInfo(folderInfo);
+        AppTarget itemTarget = mPredictionModel.getAppTargetFromInfo(itemInfo);
+        if (folderTarget != null && HotseatPredictionModel.isTrackedForPrediction(folderInfo)) {
+            notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(folderTarget,
+                    AppTargetEvent.ACTION_UNPIN, folderInfo));
         }
-        AppTarget target = getAppTargetFromItemInfo(info);
-        if (isInHotseat(info)) {
-            notifyItemAction(target, APP_LOCATION_HOTSEAT, AppTargetEvent.ACTION_PIN);
-        } else if (isInFirstPage(info)) {
-            notifyItemAction(target, APP_LOCATION_WORKSPACE, AppTargetEvent.ACTION_PIN);
+        if (itemTarget != null && HotseatPredictionModel.isTrackedForPrediction(itemInfo)) {
+            notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(itemTarget,
+                    AppTargetEvent.ACTION_PIN, itemInfo));
         }
     }
 
@@ -585,29 +521,18 @@
         }
 
         ItemInfo dragInfo = mDragObject.dragInfo;
-        ViewGroup hotseatVG = mHotseat.getShortcutsAndWidgets();
-        ViewGroup firstScreenVG = mLauncher.getWorkspace().getScreenWithId(
-                Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets();
-
-        if (dragInfo instanceof WorkspaceItemInfo
-                && dragInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
-                && dragInfo.getTargetComponent() != null) {
-            AppTarget appTarget = getAppTargetFromItemInfo(dragInfo);
-            if (!isInHotseat(dragInfo) && isInHotseat(mDragObject.originalDragInfo)) {
-                if (!getPinnedAppTargetsInViewGroup(hotseatVG).contains(appTarget)) {
-                    notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, APPTARGET_ACTION_UNPIN);
-                }
+        if (mDragObject.isMoved()) {
+            AppTarget appTarget = mPredictionModel.getAppTargetFromInfo(dragInfo);
+            //always send pin event first to prevent AiAi from predicting an item moved within
+            // the same page
+            if (appTarget != null && HotseatPredictionModel.isTrackedForPrediction(dragInfo)) {
+                notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(appTarget,
+                        AppTargetEvent.ACTION_PIN, dragInfo));
             }
-            if (!isInFirstPage(dragInfo) && isInFirstPage(mDragObject.originalDragInfo)) {
-                if (!getPinnedAppTargetsInViewGroup(firstScreenVG).contains(appTarget)) {
-                    notifyItemAction(appTarget, APP_LOCATION_WORKSPACE, APPTARGET_ACTION_UNPIN);
-                }
-            }
-            if (isInHotseat(dragInfo) && !isInHotseat(mDragObject.originalDragInfo)) {
-                notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, AppTargetEvent.ACTION_PIN);
-            }
-            if (isInFirstPage(dragInfo) && !isInFirstPage(mDragObject.originalDragInfo)) {
-                notifyItemAction(appTarget, APP_LOCATION_WORKSPACE, AppTargetEvent.ACTION_PIN);
+            if (appTarget != null && HotseatPredictionModel.isTrackedForPrediction(
+                    mDragObject.originalDragInfo)) {
+                notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(appTarget,
+                        AppTargetEvent.ACTION_UNPIN, mDragObject.originalDragInfo));
             }
         }
         mDragObject = null;
@@ -615,6 +540,7 @@
         mRequiresCacheUpdate = true;
     }
 
+
     @Nullable
     @Override
     public SystemShortcut<QuickstepLauncher> getShortcut(QuickstepLauncher activity,
@@ -711,77 +637,4 @@
                 && ((WorkspaceItemInfo) view.getTag()).container
                 == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
     }
-
-    private static boolean isPinnedIcon(View view) {
-        if (!(view instanceof BubbleTextView && view.getTag() instanceof WorkspaceItemInfo)) {
-            return false;
-        }
-        ItemInfo info = (ItemInfo) view.getTag();
-        return info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION && (
-                info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION);
-    }
-
-    private static boolean isInHotseat(ItemInfo itemInfo) {
-        return itemInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
-    }
-
-    private static boolean isInFirstPage(ItemInfo itemInfo) {
-        return itemInfo.container == LauncherSettings.Favorites.CONTAINER_DESKTOP
-                && itemInfo.screenId == Workspace.FIRST_SCREEN_ID;
-    }
-
-    private static AppTarget getAppTargetFromItemInfo(ItemInfo info) {
-        if (info.getTargetComponent() == null) return null;
-        ComponentName cn = info.getTargetComponent();
-        return new AppTarget.Builder(new AppTargetId("app:" + cn.getPackageName()),
-                cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
-    }
-
-    private AppTarget getAppTargetFromInfo(ItemInfo info) {
-        if (info == null) return null;
-        if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
-                && info instanceof LauncherAppWidgetInfo
-                && ((LauncherAppWidgetInfo) info).providerName != null) {
-            ComponentName cn = ((LauncherAppWidgetInfo) info).providerName;
-            return new AppTarget.Builder(new AppTargetId("widget:" + cn.getPackageName()),
-                    cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
-        } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
-                && info.getTargetComponent() != null) {
-            ComponentName cn = info.getTargetComponent();
-            return new AppTarget.Builder(new AppTargetId("app:" + cn.getPackageName()),
-                    cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
-        } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
-                && info instanceof WorkspaceItemInfo) {
-            ShortcutKey shortcutKey = ShortcutKey.fromItemInfo(info);
-            //TODO: switch to using full shortcut info
-            return new AppTarget.Builder(new AppTargetId("shortcut:" + shortcutKey.getId()),
-                    shortcutKey.componentName.getPackageName(), shortcutKey.user).build();
-        } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
-            return new AppTarget.Builder(new AppTargetId("folder:" + info.id),
-                    mLauncher.getPackageName(), info.user).build();
-        }
-        return null;
-    }
-
-    private AppTargetEvent wrapAppTargetWithLocation(AppTarget target, int action, ItemInfo info) {
-        return wrapAppTargetWithLocation(target, action,
-                info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
-                        ? APP_LOCATION_HOTSEAT : APP_LOCATION_WORKSPACE, info.screenId, info.cellX,
-                info.cellY, info.spanX, info.spanY);
-    }
-
-    private AppTargetEvent wrapAppTargetWithLocation(AppTarget target, int action, String root,
-            int screenId, int x, int y, int spanX, int spanY) {
-        return new AppTargetEvent.Builder(target, action).setLaunchLocation(
-                String.format(Locale.ENGLISH, "%s/%d/[%d,%d]/[%d,%d]", root, screenId, x, y, spanX,
-                        spanY)).build();
-    }
-
-    /**
-     * A helper method to generate an AppTarget that's used to communicate workspace layout
-     */
-    private AppTarget getBlockAppTarget() {
-        return new AppTarget.Builder(new AppTargetId("block"),
-                mLauncher.getPackageName(), Process.myUserHandle()).build();
-    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
new file mode 100644
index 0000000..5a038d2
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionModel.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2020 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.hybridhotseat;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.app.prediction.AppTargetId;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.model.AllAppsList;
+import com.android.launcher3.model.BaseModelUpdateTask;
+import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.PredictionModel;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.shortcuts.ShortcutKey;
+
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.function.Consumer;
+
+/**
+ * Model helper for app predictions in workspace
+ */
+public class HotseatPredictionModel extends PredictionModel {
+    private static final String APP_LOCATION_HOTSEAT = "hotseat";
+    private static final String APP_LOCATION_WORKSPACE = "workspace";
+
+    private static final String BUNDLE_KEY_PIN_EVENTS = "pin_events";
+    private static final String BUNDLE_KEY_CURRENT_ITEMS = "current_items";
+
+
+    public HotseatPredictionModel(Context context) { }
+
+    /**
+     * Creates and returns bundle using workspace items and cached items
+     */
+    public void createBundle(Consumer<Bundle> cb) {
+        LauncherAppState appState = LauncherAppState.getInstance(mContext);
+        appState.getModel().enqueueModelUpdateTask(new BaseModelUpdateTask() {
+            @Override
+            public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+                Bundle bundle = new Bundle();
+                ArrayList<AppTargetEvent> events = new ArrayList<>();
+                ArrayList<ItemInfo> workspaceItems = new ArrayList<>(dataModel.workspaceItems);
+                workspaceItems.addAll(dataModel.appWidgets);
+                for (ItemInfo item : workspaceItems) {
+                    AppTarget target = getAppTargetFromInfo(item);
+                    if (target != null && !isTrackedForPrediction(item)) continue;
+                    events.add(wrapAppTargetWithLocation(target, AppTargetEvent.ACTION_PIN, item));
+                }
+                ArrayList<AppTarget> currentTargets = new ArrayList<>();
+                for (ItemInfo itemInfo : dataModel.cachedPredictedItems) {
+                    AppTarget target = getAppTargetFromInfo(itemInfo);
+                    if (target != null) currentTargets.add(target);
+                }
+                bundle.putParcelableArrayList(BUNDLE_KEY_PIN_EVENTS, events);
+                bundle.putParcelableArrayList(BUNDLE_KEY_CURRENT_ITEMS, currentTargets);
+                MAIN_EXECUTOR.execute(() -> cb.accept(bundle));
+            }
+        });
+    }
+
+    /**
+     * Creates and returns for {@link AppTarget} object given an {@link ItemInfo}. Returns null
+     * if item is not supported prediction
+     */
+    public AppTarget getAppTargetFromInfo(ItemInfo info) {
+        if (info == null) return null;
+        if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+                && info instanceof LauncherAppWidgetInfo
+                && ((LauncherAppWidgetInfo) info).providerName != null) {
+            ComponentName cn = ((LauncherAppWidgetInfo) info).providerName;
+            return new AppTarget.Builder(new AppTargetId("widget:" + cn.getPackageName()),
+                    cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
+        } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+                && info.getTargetComponent() != null) {
+            ComponentName cn = info.getTargetComponent();
+            return new AppTarget.Builder(new AppTargetId("app:" + cn.getPackageName()),
+                    cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
+        } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+                && info instanceof WorkspaceItemInfo) {
+            ShortcutKey shortcutKey = ShortcutKey.fromItemInfo(info);
+            //TODO: switch to using full shortcut info
+            return new AppTarget.Builder(new AppTargetId("shortcut:" + shortcutKey.getId()),
+                    shortcutKey.componentName.getPackageName(), shortcutKey.user).build();
+        } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+            return new AppTarget.Builder(new AppTargetId("folder:" + info.id),
+                    mContext.getPackageName(), info.user).build();
+        }
+        return null;
+    }
+
+
+    /**
+     * Creates and returns {@link AppTargetEvent} from an {@link AppTarget}, action, and item
+     * location using {@link ItemInfo}
+     */
+    public AppTargetEvent wrapAppTargetWithLocation(AppTarget target, int action, ItemInfo info) {
+        String location = String.format(Locale.ENGLISH, "%s/%d/[%d,%d]/[%d,%d]",
+                info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
+                        ? APP_LOCATION_HOTSEAT : APP_LOCATION_WORKSPACE,
+                info.screenId, info.cellX, info.cellY, info.spanX, info.spanY);
+        return new AppTargetEvent.Builder(target, action).setLaunchLocation(location).build();
+    }
+
+    /**
+     * Helper method to determine if {@link ItemInfo} should be tracked and reported to predictors
+     */
+    public static boolean isTrackedForPrediction(ItemInfo info) {
+        return info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT || (
+                info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP
+                        && info.screenId == Workspace.FIRST_SCREEN_ID);
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 6cfc846..ad6a10b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -100,7 +100,7 @@
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
-        onStateOrResumeChanged();
+        onStateOrResumeChanging(false /* inTransition */);
     }
 
     @Override
@@ -115,11 +115,9 @@
     @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();
+                | ACTIVITY_STATE_USER_ACTIVE | ACTIVITY_STATE_TRANSITION_ACTIVE)) != 0) {
+            onStateOrResumeChanging((getActivityFlags() & ACTIVITY_STATE_TRANSITION_ACTIVE) == 0);
         }
 
         if (mHotseatPredictionController != null && ((changeBits & ACTIVITY_STATE_STARTED) != 0
@@ -164,14 +162,16 @@
     /**
      * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
      */
-    private void onStateOrResumeChanged() {
+    private void onStateOrResumeChanging(boolean inTransition) {
         LauncherState state = getStateManager().getState();
         DeviceProfile profile = getDeviceProfile();
-        boolean visible = (state == NORMAL || state == OVERVIEW) && isUserActive()
+        boolean willUserBeActive = (getActivityFlags() & ACTIVITY_STATE_USER_WILL_BE_ACTIVE) != 0;
+        boolean visible = (state == NORMAL || state == OVERVIEW)
+                && (willUserBeActive || isUserActive())
                 && !profile.isVerticalBarLayout();
         UiThreadHelper.runAsyncCommand(this, SET_SHELF_HEIGHT, visible ? 1 : 0,
                 profile.hotseatBarSizePx);
-        if (state == NORMAL) {
+        if (state == NORMAL && !inTransition) {
             ((RecentsView) getOverviewPanel()).setSwipeDownShouldLaunchApp(false);
         }
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
index 0be2486..d5b0687 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
@@ -16,7 +16,6 @@
 package com.android.launcher3.uioverrides.states;
 
 import android.content.Context;
-import android.content.res.Resources;
 import android.graphics.Rect;
 
 import com.android.launcher3.BaseDraggingActivity;
@@ -58,8 +57,6 @@
     }
 
     public static float[] getOverviewScaleAndOffsetForModalState(BaseDraggingActivity activity) {
-        Resources res = activity.getResources();
-
         Rect out = new Rect();
         activity.<RecentsView>getOverviewPanel().getTaskSize(out);
         int taskHeight = out.height();
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 1dd5fb7..f38ff10 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -31,9 +31,9 @@
 import android.view.View;
 
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.quickstep.util.AppWindowAnimationHelper;
 import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.util.TransformParams;
@@ -47,20 +47,21 @@
  *
  * @param <T> activity that contains the overview
  */
-final class AppToOverviewAnimationProvider<T extends BaseDraggingActivity> extends
+final class AppToOverviewAnimationProvider<T extends StatefulActivity<?>> extends
         RemoteAnimationProvider {
 
     private static final long RECENTS_LAUNCH_DURATION = 250;
     private static final String TAG = "AppToOverviewAnimationProvider";
 
-    private final BaseActivityInterface<T> mActivityInterface;
+    private final BaseActivityInterface<?, T> mActivityInterface;
     // The id of the currently running task that is transitioning to overview.
     private final int mTargetTaskId;
 
     private T mActivity;
     private RecentsView mRecentsView;
 
-    AppToOverviewAnimationProvider(BaseActivityInterface<T> activityInterface, int targetTaskId) {
+    AppToOverviewAnimationProvider(
+            BaseActivityInterface<?, T> activityInterface, int targetTaskId) {
         mActivityInterface = activityInterface;
         mTargetTaskId = targetTaskId;
     }
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 76c6060..bbee67c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -44,12 +44,12 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.views.FloatingIconView;
@@ -60,7 +60,6 @@
 import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.util.TransformParams.BuilderProxy;
-import com.android.quickstep.util.WindowSizeStrategy;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -76,7 +75,7 @@
  * Base class for swipe up handler with some utility methods
  */
 @TargetApi(Build.VERSION_CODES.Q)
-public abstract class BaseSwipeUpHandler<T extends BaseDraggingActivity, Q extends RecentsView>
+public abstract class BaseSwipeUpHandler<T extends StatefulActivity<?>, Q extends RecentsView>
         implements RecentsAnimationListener {
 
     private static final String TAG = "BaseSwipeUpHandler";
@@ -97,7 +96,7 @@
     protected final Context mContext;
     protected final RecentsAnimationDeviceState mDeviceState;
     protected final GestureState mGestureState;
-    protected final BaseActivityInterface<T> mActivityInterface;
+    protected final BaseActivityInterface<?, T> mActivityInterface;
     protected final InputConsumerController mInputConsumer;
 
     protected final TaskViewSimulator mTaskViewSimulator;
@@ -132,15 +131,14 @@
     private boolean mRecentsViewScrollLinked = false;
 
     protected BaseSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
-            GestureState gestureState, InputConsumerController inputConsumer,
-            WindowSizeStrategy windowSizeStrategy) {
+            GestureState gestureState, InputConsumerController inputConsumer) {
         mContext = context;
         mDeviceState = deviceState;
         mGestureState = gestureState;
         mActivityInterface = gestureState.getActivityInterface();
         mActivityInitListener = mActivityInterface.createActivityInitListener(this::onActivityInit);
         mInputConsumer = inputConsumer;
-        mTaskViewSimulator = new TaskViewSimulator(context, windowSizeStrategy);
+        mTaskViewSimulator = new TaskViewSimulator(context, gestureState.getActivityInterface());
     }
 
     /**
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 cd546e2..4b3af31 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
@@ -20,20 +20,22 @@
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 import static com.android.quickstep.fallback.RecentsState.BACKGROUND_APP;
 import static com.android.quickstep.fallback.RecentsState.DEFAULT;
-import static com.android.quickstep.util.WindowSizeStrategy.FALLBACK_RECENTS_SIZE_STRATEGY;
 import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 
 import android.content.Context;
+import android.graphics.PointF;
 import android.graphics.Rect;
 
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.fallback.RecentsState;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -46,19 +48,18 @@
  * currently running one and apps should interact with the {@link RecentsActivity} as opposed
  * to the in-launcher one.
  */
-public final class FallbackActivityInterface implements
-        BaseActivityInterface<RecentsActivity> {
+public final class FallbackActivityInterface extends
+        BaseActivityInterface<RecentsState, RecentsActivity> {
 
-    public FallbackActivityInterface() { }
+    public static final FallbackActivityInterface INSTANCE = new FallbackActivityInterface();
 
-    @Override
-    public void onTransitionCancelled(boolean activityVisible) {
-        // TODO:
+    private FallbackActivityInterface() {
+        super(false);
     }
 
     @Override
     public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
-        FALLBACK_RECENTS_SIZE_STRATEGY.calculateTaskSize(context, dp, outRect);
+        calculateTaskSize(context, dp, outRect);
         if (dp.isVerticalBarLayout()
                 && SysUINavigationMode.INSTANCE.get(context).getMode() != NO_BUTTON) {
             Rect targetInsets = dp.getInsets();
@@ -70,17 +71,6 @@
     }
 
     @Override
-    public void onSwipeUpToRecentsComplete() {
-        RecentsActivity activity = getCreatedActivity();
-        if (activity == null) {
-            return;
-        }
-        RecentsView recentsView = activity.getOverviewPanel();
-        recentsView.getClearAllButton().setVisibilityAlpha(1);
-        recentsView.setDisallowScrollToClearAll(false);
-    }
-
-    @Override
     public void onAssistantVisibilityChanged(float visibility) {
         // This class becomes active when the screen is locked.
         // Rather than having it handle assistant visibility changes, the assistant visibility is
@@ -198,11 +188,14 @@
     }
 
     @Override
-    public void onLaunchTaskSuccess() {
-        RecentsActivity activity = getCreatedActivity();
-        if (activity == null) {
-            return;
-        }
-        activity.onTaskLaunched();
+    public void getMultiWindowSize(Context context, DeviceProfile dp, PointF out) {
+        out.set(dp.widthPx, dp.heightPx);
+    }
+
+    @Override
+    protected float getExtraSpace(Context context, DeviceProfile dp) {
+        return showOverviewActions(context)
+                ? context.getResources().getDimensionPixelSize(R.dimen.overview_actions_height)
+                : 0;
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
index 77e50ca..db41bd6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -24,7 +24,6 @@
 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.util.WindowSizeStrategy.FALLBACK_RECENTS_SIZE_STRATEGY;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 
 import android.animation.Animator;
@@ -114,7 +113,7 @@
     public FallbackSwipeHandler(Context context, RecentsAnimationDeviceState deviceState,
             GestureState gestureState, InputConsumerController inputConsumer,
             boolean isLikelyToStartNewTask, boolean continuingLastGesture) {
-        super(context, deviceState, gestureState, inputConsumer, FALLBACK_RECENTS_SIZE_STRATEGY);
+        super(context, deviceState, gestureState, inputConsumer);
 
         mInQuickSwitchMode = isLikelyToStartNewTask || continuingLastGesture;
         mContinuingLastGesture = continuingLastGesture;
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 9ff5d942..4ebfbd6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -21,16 +21,21 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.anim.Interpolators.INSTANT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
 import static com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory.INDEX_RECENTS_FADE_ANIM;
 import static com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory.INDEX_RECENTS_TRANSLATE_X_ANIM;
 import static com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory.INDEX_SHELF_ANIM;
 import static com.android.quickstep.LauncherSwipeHandler.RECENTS_ATTACH_DURATION;
-import static com.android.quickstep.util.WindowSizeStrategy.LAUNCHER_ACTIVITY_SIZE_STRATEGY;
+import static com.android.quickstep.SysUINavigationMode.getMode;
+import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
+import static com.android.quickstep.util.LayoutUtils.getDefaultSwipeHeight;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 
 import android.animation.Animator;
 import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.util.Log;
 import android.view.MotionEvent;
@@ -44,6 +49,7 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherInitListener;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
@@ -60,7 +66,6 @@
 import com.android.quickstep.views.LauncherRecentsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.plugins.shared.LauncherOverlayManager;
-import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
 import java.util.function.Consumer;
@@ -69,11 +74,18 @@
 /**
  * {@link BaseActivityInterface} for the in-launcher recents.
  */
-public final class LauncherActivityInterface implements BaseActivityInterface<Launcher> {
+public final class LauncherActivityInterface extends
+        BaseActivityInterface<LauncherState, Launcher> {
+
+    public static final LauncherActivityInterface INSTANCE = new LauncherActivityInterface();
+
+    private LauncherActivityInterface() {
+        super(true);
+    }
 
     @Override
     public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
-        LAUNCHER_ACTIVITY_SIZE_STRATEGY.calculateTaskSize(context, dp, outRect);
+        calculateTaskSize(context, dp, outRect);
         if (dp.isVerticalBarLayout() && SysUINavigationMode.getMode(context) != Mode.NO_BUTTON) {
             Rect targetInsets = dp.getInsets();
             int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
@@ -84,24 +96,12 @@
     }
 
     @Override
-    public void onTransitionCancelled(boolean activityVisible) {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
-        LauncherState startState = launcher.getStateManager().getRestState();
-        launcher.getStateManager().goToState(startState, activityVisible);
-    }
-
-    @Override
     public void onSwipeUpToRecentsComplete() {
-        // Re apply state in case we did something funky during the transition.
+        super.onSwipeUpToRecentsComplete();
         Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
+        if (launcher != null) {
+            DiscoveryBounce.showForOverviewIfNeeded(launcher);
         }
-        launcher.getStateManager().reapplyState();
-        DiscoveryBounce.showForOverviewIfNeeded(launcher);
     }
 
     @Override
@@ -113,15 +113,7 @@
         // 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();
-        setLauncherHideBackArrow(false);
-    }
-
-    private void setLauncherHideBackArrow(boolean hideBackArrow) {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
-        launcher.getRootView().setForceHideBackArrow(hideBackArrow);
+        launcher.getRootView().setForceHideBackArrow(false);
     }
 
     @Override
@@ -299,6 +291,11 @@
     }
 
     @Override
+    public void setHintUserWillBeActive() {
+        getCreatedActivity().setHintUserWillBeActive();
+    }
+
+    @Override
     public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
         return deviceState.isInDeferredGestureRegion(ev);
     }
@@ -337,15 +334,6 @@
     }
 
     @Override
-    public void onLaunchTaskSuccess() {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
-        launcher.getStateManager().moveToRestState();
-    }
-
-    @Override
     public void closeOverlay() {
         Launcher launcher = getCreatedActivity();
         if (launcher == null) {
@@ -360,23 +348,6 @@
     }
 
     @Override
-    public void switchRunningTaskViewToScreenshot(ThumbnailData thumbnailData,
-            Runnable onFinishRunnable) {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
-        RecentsView recentsView = launcher.getOverviewPanel();
-        if (recentsView == null) {
-            if (onFinishRunnable != null) {
-                onFinishRunnable.run();
-            }
-            return;
-        }
-        recentsView.switchToScreenshot(thumbnailData, onFinishRunnable);
-    }
-
-    @Override
     public void setOnDeferredActivityLaunchCallback(Runnable r) {
         Launcher launcher = getCreatedActivity();
         if (launcher == null) {
@@ -404,4 +375,50 @@
         }
         return launcher.getDepthController();
     }
+
+    @Override
+    public void getMultiWindowSize(Context context, DeviceProfile dp, PointF out) {
+        DeviceProfile fullDp = dp.getFullScreenProfile();
+        // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to
+        // account for system insets
+        out.set(fullDp.availableWidthPx, fullDp.availableHeightPx);
+        float halfDividerSize = context.getResources()
+                .getDimension(R.dimen.multi_window_task_divider_size) / 2;
+
+        if (fullDp.isLandscape) {
+            out.x = out.x / 2 - halfDividerSize;
+        } else {
+            out.y = out.y / 2 - halfDividerSize;
+        }
+    }
+
+    @Override
+    protected float getExtraSpace(Context context, DeviceProfile dp) {
+        if (dp.isVerticalBarLayout()) {
+            return  0;
+        } else {
+            Resources res = context.getResources();
+            if (showOverviewActions(context)) {
+                //TODO: this needs to account for the swipe gesture height and accessibility
+                // UI when shown.
+                float actionsBottomMargin = 0;
+                if (getMode(context) == Mode.THREE_BUTTONS) {
+                    actionsBottomMargin = res.getDimensionPixelSize(
+                            R.dimen.overview_actions_bottom_margin_three_button);
+                } else {
+                    actionsBottomMargin = res.getDimensionPixelSize(
+                            R.dimen.overview_actions_bottom_margin_gesture);
+                }
+                float actionsHeight = actionsBottomMargin
+                        + res.getDimensionPixelSize(R.dimen.overview_actions_height);
+                return actionsHeight;
+            } else {
+                return getDefaultSwipeHeight(context, dp) + dp.workspacePageIndicatorHeight
+                        + res.getDimensionPixelSize(
+                        R.dimen.dynamic_grid_hotseat_extra_vertical_size)
+                        + res.getDimensionPixelSize(
+                        R.dimen.dynamic_grid_hotseat_bottom_padding);
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
index 0bf81ef..82a3e79 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
@@ -35,7 +35,6 @@
 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.util.WindowSizeStrategy.LAUNCHER_ACTIVITY_SIZE_STRATEGY;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 
 import android.animation.Animator;
@@ -60,7 +59,6 @@
 import androidx.annotation.UiThread;
 
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
@@ -69,6 +67,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -198,7 +197,7 @@
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
             long touchTimeMs, boolean continuingLastGesture,
             InputConsumerController inputConsumer) {
-        super(context, deviceState, gestureState, inputConsumer, LAUNCHER_ACTIVITY_SIZE_STRATEGY);
+        super(context, deviceState, gestureState, inputConsumer);
         mTaskAnimationManager = taskAnimationManager;
         mTouchTimeMs = touchTimeMs;
         mContinuingLastGesture = continuingLastGesture;
@@ -587,9 +586,8 @@
             boolean quickswitchThresholdPassed = centermostTask != runningTask;
 
             // We will handle the sysui flags based on the centermost task view.
-            mRecentsAnimationController.setUseLauncherSystemBarFlags(
-                    (swipeUpThresholdPassed || quickswitchThresholdPassed)
-                            && centermostTaskFlags != 0);
+            mRecentsAnimationController.setUseLauncherSystemBarFlags(swipeUpThresholdPassed
+                    ||  (quickswitchThresholdPassed && centermostTaskFlags != 0));
             mRecentsAnimationController.setSplitScreenMinimized(swipeUpThresholdPassed);
 
             int sysuiFlags = swipeUpThresholdPassed ? 0 : centermostTaskFlags;
@@ -684,7 +682,7 @@
             setTargetAlphaProvider(LauncherSwipeHandler::getHiddenTargetAlpha);
         }
 
-        BaseDraggingActivity activity = mActivityInterface.getCreatedActivity();
+        StatefulActivity activity = mActivityInterface.getCreatedActivity();
         return activity == null ? InputConsumer.NO_OP
                 : new OverviewInputConsumer(mGestureState, activity, null, true);
     }
@@ -940,6 +938,7 @@
                         : null;
 
                 mActivity.getRootView().setForceHideBackArrow(true);
+                mActivityInterface.setHintUserWillBeActive();
 
                 homeAnimFactory = new HomeAnimationFactory(floatingIconView) {
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LockScreenRecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LockScreenRecentsActivity.java
deleted file mode 100644
index 65f323c..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LockScreenRecentsActivity.java
+++ /dev/null
@@ -1,31 +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 android.app.Activity;
-import android.os.Bundle;
-
-/**
- * Empty activity to start a recents transition
- */
-public class LockScreenRecentsActivity extends Activity {
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        finish();
-    }
-}
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 c6b719a..8846727 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
@@ -28,9 +28,10 @@
 import android.view.ViewConfiguration;
 
 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.statemanager.StatefulActivity;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.RemoteAnimationProvider;
@@ -111,7 +112,7 @@
         protected void onTransitionComplete() {
             // TODO(b/138729100) This doesn't execute first time launcher is run
             if (mTriggeredFromAltTab) {
-                RecentsView rv = (RecentsView) mActivityInterface.getVisibleRecentsView();
+                RecentsView rv =  mActivityInterface.getVisibleRecentsView();
                 if (rv == null) {
                     return;
                 }
@@ -136,7 +137,7 @@
 
         @Override
         protected boolean handleCommand(long elapsedTime) {
-            RecentsView recents = (RecentsView) mActivityInterface.getVisibleRecentsView();
+            RecentsView recents = mActivityInterface.getVisibleRecentsView();
             if (recents == null) {
                 return false;
             }
@@ -150,9 +151,9 @@
         }
     }
 
-    private class RecentsActivityCommand<T extends BaseDraggingActivity> implements Runnable {
+    private class RecentsActivityCommand<T extends StatefulActivity<?>> implements Runnable {
 
-        protected final BaseActivityInterface<T> mActivityInterface;
+        protected final BaseActivityInterface<?, T> mActivityInterface;
         private final long mCreateTime;
         private final AppToOverviewAnimationProvider<T> mAnimationProvider;
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
index 03d522e..a4670fd 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
@@ -261,10 +261,6 @@
         AccessibilityManagerCompat.sendStateEventToTest(getBaseContext(), OVERVIEW_STATE_ORDINAL);
     }
 
-    public void onTaskLaunched() {
-        mFallbackRecentsView.resetTaskVisuals();
-    }
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
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 258d60c..acc7794 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -63,6 +63,7 @@
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.provider.RestoreDbTask;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.tracing.nano.LauncherTraceProto;
@@ -705,7 +706,7 @@
     public InputConsumer createOverviewInputConsumer(GestureState previousGestureState,
             GestureState gestureState, MotionEvent event,
             boolean forceOverviewInputConsumer) {
-        BaseDraggingActivity activity = gestureState.getActivityInterface().getCreatedActivity();
+        StatefulActivity activity = gestureState.getActivityInterface().getCreatedActivity();
         if (activity == null) {
             return mResetGestureInputConsumer;
         }
@@ -754,7 +755,7 @@
             return;
         }
 
-        final BaseActivityInterface<BaseDraggingActivity> activityInterface =
+        final BaseActivityInterface activityInterface =
                 mOverviewComponentObserver.getActivityInterface();
         final Intent overviewIntent = new Intent(
                 mOverviewComponentObserver.getOverviewIntentIgnoreSysUiState());
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 1ab317b..f958e6d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -17,7 +17,6 @@
 
 import static com.android.quickstep.fallback.RecentsState.DEFAULT;
 import static com.android.quickstep.fallback.RecentsState.MODAL_TASK;
-import static com.android.quickstep.util.WindowSizeStrategy.FALLBACK_RECENTS_SIZE_STRATEGY;
 
 import android.annotation.TargetApi;
 import android.app.ActivityManager.RunningTaskInfo;
@@ -26,6 +25,7 @@
 import android.util.AttributeSet;
 
 import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.quickstep.FallbackActivityInterface;
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
@@ -45,7 +45,7 @@
     }
 
     public FallbackRecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr, FALLBACK_RECENTS_SIZE_STRATEGY);
+        super(context, attrs, defStyleAttr, FallbackActivityInterface.INSTANCE);
         mActivity.getStateManager().addStateListener(this);
     }
 
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 adf19df..2dc7f5f 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
@@ -18,14 +18,15 @@
 import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_POINTER_DOWN;
 import static android.view.MotionEvent.ACTION_UP;
-
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.Utilities.squaredTouchSlop;
 import static com.android.quickstep.LauncherSwipeHandler.MIN_PROGRESS_FOR_OVERVIEW;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 
-import android.content.ComponentName;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Point;
@@ -34,15 +35,14 @@
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
-
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.DefaultDisplay;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
-import com.android.quickstep.LockScreenRecentsActivity;
 import com.android.quickstep.MultiStateCallback;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationController;
@@ -52,6 +52,7 @@
 import com.android.quickstep.util.AppWindowAnimationHelper;
 import com.android.quickstep.util.TransformParams;
 import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputMonitorCompat;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
@@ -61,8 +62,6 @@
 public class DeviceLockedInputConsumer implements InputConsumer,
         RecentsAnimationCallbacks.RecentsAnimationListener {
 
-    private static final float SCALE_DOWN = 0.75f;
-
     private static final String[] STATE_NAMES = DEBUG_STATES ? new String[2] : null;
     private static int getFlagForIndex(int index, String name) {
         if (DEBUG_STATES) {
@@ -93,6 +92,7 @@
     private float mProgress;
 
     private boolean mThresholdCrossed = false;
+    private boolean mHomeLaunched = false;
 
     private RecentsAnimationController mRecentsAnimationController;
     private RecentsAnimationTargets mRecentsAnimationTargets;
@@ -176,7 +176,6 @@
      * the animation can still be running.
      */
     private void finishTouchTracking(MotionEvent ev) {
-        mStateCallback.setState(STATE_HANDLER_INVALIDATED);
         if (mThresholdCrossed && ev.getAction() == ACTION_UP) {
             mVelocityTracker.computeCurrentVelocity(1000,
                     ViewConfiguration.get(mContext).getScaledMaximumFlingVelocity());
@@ -192,12 +191,34 @@
             } else {
                 dismissTask = mProgress >= (1 - MIN_PROGRESS_FOR_OVERVIEW);
             }
-            if (dismissTask) {
-                // For now, just start the home intent so user is prompted to unlock the device.
-                mContext.startActivity(new Intent(Intent.ACTION_MAIN)
-                        .addCategory(Intent.CATEGORY_HOME)
-                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
-            }
+
+            // Animate back to fullscreen before finishing
+            ValueAnimator animator = ValueAnimator.ofFloat(mTransformParams.getProgress(), 0f);
+            animator.setDuration(100);
+            animator.setInterpolator(Interpolators.ACCEL);
+            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                    mTransformParams.setProgress((float) valueAnimator.getAnimatedValue());
+                    mAppWindowAnimationHelper.applyTransform(mTransformParams);
+                }
+            });
+            animator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    if (dismissTask) {
+                        // For now, just start the home intent so user is prompted to unlock the device.
+                        mContext.startActivity(new Intent(Intent.ACTION_MAIN)
+                                .addCategory(Intent.CATEGORY_HOME)
+                                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+                        mHomeLaunched = true;
+                    }
+                    mStateCallback.setState(STATE_HANDLER_INVALIDATED);
+                }
+            });
+            animator.start();
+        } else {
+            mStateCallback.setState(STATE_HANDLER_INVALIDATED);
         }
         mVelocityTracker.recycle();
         mVelocityTracker = null;
@@ -205,13 +226,11 @@
 
     private void startRecentsTransition() {
         mThresholdCrossed = true;
+        mHomeLaunched = false;
         TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
         mInputMonitorCompat.pilferPointers();
 
-        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)
+        Intent intent = mGestureState.getHomeIntent()
                 .putExtra(INTENT_EXTRA_LOG_TRACE_ID, mGestureState.getGestureId());
         mTaskAnimationManager.startRecentsAnimation(mGestureState, intent, this);
     }
@@ -229,8 +248,9 @@
             mAppWindowAnimationHelper.updateSource(displaySize, targetCompat);
         }
 
-        Utilities.scaleRectAboutCenter(displaySize, SCALE_DOWN);
-        displaySize.offsetTo(displaySize.left, 0);
+        // Offset the surface slightly
+        displaySize.offset(0, mContext.getResources().getDimensionPixelSize(
+                R.dimen.device_locked_y_offset));
         mTransformParams.setTargetSet(mRecentsAnimationTargets);
         mAppWindowAnimationHelper.updateTargetRect(displaySize);
         mAppWindowAnimationHelper.applyTransform(mTransformParams);
@@ -245,7 +265,9 @@
     }
 
     private void endRemoteAnimation() {
-        if (mRecentsAnimationController != null) {
+        if (mHomeLaunched) {
+            ActivityManagerWrapper.getInstance().cancelRecentsAnimation(false);
+        } else if (mRecentsAnimationController != null) {
             mRecentsAnimationController.finishController(
                     false /* toRecents */, null /* callback */, false /* sendUserLeaveHint */);
         }
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 1f6c506..6b0d7a3 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,6 +26,7 @@
 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.GestureState.STATE_OVERSCROLL_WINDOW_CREATED;
 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;
 
@@ -430,6 +431,6 @@
 
     @Override
     public boolean allowInterceptByParent() {
-        return !mPassedPilferInputSlop;
+        return !mPassedPilferInputSlop || mGestureState.hasState(STATE_OVERSCROLL_WINDOW_CREATED);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
index c49b8f2..fb420a2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
@@ -24,12 +24,15 @@
 
 import static com.android.launcher3.Utilities.squaredHypot;
 
+import static java.lang.Math.abs;
+
 import android.content.Context;
 import android.graphics.PointF;
-import android.view.GestureDetector;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BaseDraggingActivity;
@@ -44,24 +47,36 @@
  * Input consumer for handling events to pass to an {@code OverscrollPlugin}.
  */
 public class OverscrollInputConsumer extends DelegateInputConsumer {
-
     private static final String TAG = "OverscrollInputConsumer";
+    private static final boolean DEBUG_LOGS_ENABLED = false;
+    private static void debugPrint(String log) {
+        if (DEBUG_LOGS_ENABLED) {
+            Log.v(TAG, log);
+        }
+    }
 
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
     private final PointF mStartDragPos = new PointF();
     private final int mAngleThreshold;
 
-    private final float mFlingThresholdPx;
+    private final int mFlingDistanceThresholdPx;
+    private final int mFlingVelocityThresholdPx;
     private int mActivePointerId = -1;
     private boolean mPassedSlop = false;
-
+    // True if we set ourselves as active, meaning we no longer pass events to the delegate.
+    private boolean mPassedActiveThreshold = false;
+    // When a gesture crosses this length, this recognizer will attempt to interpret touch events.
     private final float mSquaredSlop;
+    // When a gesture crosses this length, this recognizer will become the sole active recognizer.
+    private final float mSquaredActiveThreshold;
+    // When a gesture crosses this length, the overscroll view should be shown.
+    private final float mSquaredFinishThreshold;
+    private boolean mThisDownIsIgnored = false;
 
     private final GestureState mGestureState;
     @Nullable
     private final OverscrollPlugin mPlugin;
-    private final GestureDetector mGestureDetector;
 
     @Nullable
     private RecentsView mRecentsView;
@@ -72,15 +87,24 @@
 
         mAngleThreshold = context.getResources()
                 .getInteger(R.integer.assistant_gesture_corner_deg_threshold);
-        mFlingThresholdPx = context.getResources()
-            .getDimension(R.dimen.gestures_overscroll_fling_threshold);
+        mFlingDistanceThresholdPx = (int) context.getResources()
+                .getDimension(R.dimen.gestures_overscroll_fling_threshold);
+        mFlingVelocityThresholdPx = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
         mGestureState = gestureState;
         mPlugin = plugin;
 
         float slop = ViewConfiguration.get(context).getScaledTouchSlop();
 
         mSquaredSlop = slop * slop;
-        mGestureDetector = new GestureDetector(context, new FlingGestureListener());
+
+
+        float finishGestureThreshold = (int) context.getResources()
+                .getDimension(R.dimen.gestures_overscroll_finish_threshold);
+        mSquaredFinishThreshold = finishGestureThreshold * finishGestureThreshold;
+
+        float activeThreshold = (int) context.getResources()
+                .getDimension(R.dimen.gestures_overscroll_active_threshold);
+        mSquaredActiveThreshold = activeThreshold * activeThreshold;
     }
 
     @Override
@@ -90,12 +114,26 @@
 
     @Override
     public void onMotionEvent(MotionEvent ev) {
+        if (mPlugin == null) {
+            return;
+        }
+
+        debugPrint("got event, underlying activity is " + getUnderlyingActivity());
         switch (ev.getActionMasked()) {
             case ACTION_DOWN: {
+                debugPrint("ACTION_DOWN");
                 mActivePointerId = ev.getPointerId(0);
                 mDownPos.set(ev.getX(), ev.getY());
                 mLastPos.set(mDownPos);
-
+                if (mPlugin.blockOtherGestures()) {
+                    debugPrint("mPlugin.blockOtherGestures(), becoming active on ACTION_DOWN");
+                    // Otherwise, if an appear gesture is performed when the Activity is visible,
+                    // the Activity will dismiss its keyboard.
+                    mPassedActiveThreshold = true;
+                    mPassedSlop = true;
+                    mStartDragPos.set(mLastPos.x, mLastPos.y);
+                    setActive(ev);
+                }
                 break;
             }
             case ACTION_POINTER_DOWN: {
@@ -121,57 +159,61 @@
                 if (mState == STATE_DELEGATE_ACTIVE) {
                     break;
                 }
+
                 if (!mDelegate.allowInterceptByParent()) {
                     mState = STATE_DELEGATE_ACTIVE;
                     break;
                 }
+
+                // Update last touch position.
                 int pointerIndex = ev.findPointerIndex(mActivePointerId);
                 if (pointerIndex == -1) {
                     break;
                 }
                 mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
 
-                if (!mPassedSlop) {
-                    // Normal gesture, ensure we pass the slop before we start tracking the gesture
-                    if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
-                            > mSquaredSlop) {
-
-                        mPassedSlop = true;
-                        mStartDragPos.set(mLastPos.x, mLastPos.y);
-                        if (isOverscrolled()) {
-                            setActive(ev);
-
-                            if (mPlugin != null) {
-                                mPlugin.onTouchStart(getDeviceState(), getUnderlyingActivity());
-                            }
-                        } else {
-                            mState = STATE_DELEGATE_ACTIVE;
-                        }
-                    }
+                float squaredDist = squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y);
+                if ((!mPassedSlop) && (squaredDist > mSquaredSlop)) {
+                    mPassedSlop = true;
+                    mStartDragPos.set(mLastPos.x, mLastPos.y);
+                    mGestureState.setState(GestureState.STATE_OVERSCROLL_WINDOW_CREATED);
                 }
 
-                if (mPassedSlop && mState != STATE_DELEGATE_ACTIVE && isOverscrolled()
-                        && mPlugin != null) {
-                    mPlugin.onTouchTraveled(getDistancePx());
+                boolean becomeActive = mPassedSlop && !mPassedActiveThreshold && isOverscrolled()
+                        && (squaredDist > mSquaredActiveThreshold);
+                if (becomeActive) {
+                    debugPrint("Past slop and past threshold, set active");
+                    mPassedActiveThreshold = true;
+                    setActive(ev);
+                }
+
+                if (mPassedActiveThreshold) {
+                    debugPrint("ACTION_MOVE Relaying touch event");
+                    mPlugin.onTouchEvent(ev, getHorizontalDistancePx(), getVerticalDistancePx(),
+                            (int) Math.sqrt(mSquaredFinishThreshold), mFlingDistanceThresholdPx,
+                            mFlingVelocityThresholdPx, getDeviceState(), getUnderlyingActivity());
                 }
 
                 break;
             }
             case ACTION_CANCEL:
             case ACTION_UP:
-                if (mState != STATE_DELEGATE_ACTIVE && mPassedSlop && mPlugin != null) {
-                    mPlugin.onTouchEnd(getDistancePx());
+                debugPrint("ACTION_UP");
+                if (mPassedActiveThreshold) {
+                    debugPrint("ACTION_UP Relaying touch event");
+
+                    mPlugin.onTouchEvent(ev, getHorizontalDistancePx(), getVerticalDistancePx(),
+                            (int) Math.sqrt(mSquaredFinishThreshold), mFlingDistanceThresholdPx,
+                            mFlingVelocityThresholdPx, getDeviceState(), getUnderlyingActivity());
                 }
 
+
                 mPassedSlop = false;
+                mPassedActiveThreshold = false;
                 mState = STATE_INACTIVE;
                 break;
         }
 
-        if (mState != STATE_DELEGATE_ACTIVE) {
-            mGestureDetector.onTouchEvent(ev);
-        }
-
         if (mState != STATE_ACTIVE) {
             mDelegate.onMotionEvent(ev);
         }
@@ -192,15 +234,20 @@
             maxIndex = 1;
         }
 
-        boolean atRightMostApp = (mRecentsView == null
-                || mRecentsView.getRunningTaskIndex() <= maxIndex);
+        boolean atRightMostApp = mRecentsView == null
+                || (mRecentsView.getRunningTaskIndex() <= maxIndex);
 
         // Check if the gesture is within our angle threshold of horizontal
-        float deltaY = Math.abs(mLastPos.y - mDownPos.y);
-        float deltaX = mDownPos.x - mLastPos.x; // Positive if this is a gesture to the left
-        boolean angleInBounds = Math.toDegrees(Math.atan2(deltaY, deltaX)) < mAngleThreshold;
+        float deltaY = abs(mLastPos.y - mDownPos.y);
+        float deltaX = mLastPos.x - mDownPos.x;
 
-        return atRightMostApp && angleInBounds;
+        boolean angleInBounds = (Math.toDegrees(Math.atan2(deltaY, abs(deltaX))) < mAngleThreshold);
+
+        boolean overscrollVisible = mPlugin.blockOtherGestures();
+        boolean overscrollInvisibleAndLeftSwipe = !overscrollVisible && deltaX < 0;
+        boolean gestureDirectionMatchesVisibility = overscrollVisible
+                || overscrollInvisibleAndLeftSwipe;
+        return atRightMostApp && angleInBounds && gestureDirectionMatchesVisibility;
     }
 
     private String getDeviceState() {
@@ -219,35 +266,22 @@
         return deviceState;
     }
 
-    private int getDistancePx() {
-        return (int) Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y);
+    private int getHorizontalDistancePx() {
+        return (int) (mLastPos.x - mDownPos.x);
     }
 
-    private String getUnderlyingActivity() {
+    private int getVerticalDistancePx() {
+        return (int) (mLastPos.y - mDownPos.y);
+    }
+
+    private @NonNull String getUnderlyingActivity() {
+        // Overly defensive, got guidance on code review that something in the chain of
+        // `mGestureState.getRunningTask().topActivity` can be null and thus cause a null pointer
+        // exception to be thrown, but we aren't sure which part can be null.
+        if ((mGestureState == null) || (mGestureState.getRunningTask() == null)
+                || (mGestureState.getRunningTask().topActivity == null)) {
+            return "";
+        }
         return mGestureState.getRunningTask().topActivity.flattenToString();
     }
-
-    private class FlingGestureListener extends GestureDetector.SimpleOnGestureListener {
-        @Override
-        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
-            if (isValidAngle(velocityX, -velocityY)
-                    && getDistancePx() >= mFlingThresholdPx
-                    && mState != STATE_DELEGATE_ACTIVE) {
-
-                if (mPlugin != null) {
-                    mPlugin.onFling(-velocityX);
-                }
-            }
-            return true;
-        }
-
-        private boolean isValidAngle(float deltaX, float deltaY) {
-            float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
-            // normalize so that angle is measured clockwise from horizontal in the bottom right
-            // corner and counterclockwise from horizontal in the bottom left corner
-
-            angle = angle > 90 ? 180 - angle : angle;
-            return (angle < mAngleThreshold);
-        }
-    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
index c82d4b5..11fee2f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
@@ -24,8 +24,8 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.views.BaseDragLayer;
@@ -41,11 +41,11 @@
 /**
  * Input consumer for handling touch on the recents/Launcher activity.
  */
-public class OverviewInputConsumer<T extends BaseDraggingActivity>
+public class OverviewInputConsumer<T extends StatefulActivity<?>>
         implements InputConsumer {
 
     private final T mActivity;
-    private final BaseActivityInterface<T> mActivityInterface;
+    private final BaseActivityInterface<?, T> mActivityInterface;
     private final BaseDragLayer mTarget;
     private final InputMonitorCompat mInputMonitor;
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
index b8f0f4d..a3db940 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -15,8 +15,6 @@
  */
 package com.android.quickstep.util;
 
-import static android.view.Surface.ROTATION_0;
-
 import static com.android.launcher3.states.RotationHelper.deltaRotation;
 import static com.android.launcher3.touch.PagedOrientationHandler.MATRIX_POST_TRANSLATE;
 import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
@@ -33,6 +31,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.views.RecentsView.ScrollState;
 import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper;
 import com.android.quickstep.views.TaskView;
@@ -52,7 +51,7 @@
 
     private final RecentsOrientedState mOrientationState;
     private final Context mContext;
-    private final WindowSizeStrategy mSizeStrategy;
+    private final BaseActivityInterface mSizeStrategy;
 
     private final Rect mTaskRect = new Rect();
     private final PointF mPivot = new PointF();
@@ -81,7 +80,7 @@
     private boolean mLayoutValid = false;
     private boolean mScrollValid = false;
 
-    public TaskViewSimulator(Context context, WindowSizeStrategy sizeStrategy) {
+    public TaskViewSimulator(Context context, BaseActivityInterface sizeStrategy) {
         mContext = context;
         mSizeStrategy = sizeStrategy;
 
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 250c78b..3d89403 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
@@ -25,7 +25,6 @@
 import static com.android.launcher3.QuickstepAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.quickstep.util.WindowSizeStrategy.LAUNCHER_ACTIVITY_SIZE_STRATEGY;
 
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
@@ -47,6 +46,7 @@
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.views.ScrimView;
+import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.util.TransformParams;
 import com.android.systemui.plugins.PluginListener;
@@ -89,7 +89,7 @@
     }
 
     public LauncherRecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr, LAUNCHER_ACTIVITY_SIZE_STRATEGY);
+        super(context, attrs, defStyleAttr, LauncherActivityInterface.INSTANCE);
         mActivity.getStateManager().addStateListener(this);
     }
 
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 54ed40f..253e83c 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
@@ -115,6 +115,7 @@
 import com.android.launcher3.util.OverScroller;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.ViewPool;
+import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.RecentsAnimationController;
 import com.android.quickstep.RecentsAnimationTargets;
 import com.android.quickstep.RecentsModel;
@@ -128,7 +129,6 @@
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.SplitScreenBounds;
 import com.android.quickstep.util.TransformParams;
-import com.android.quickstep.util.WindowSizeStrategy;
 import com.android.systemui.plugins.ResourceProvider;
 import com.android.systemui.shared.recents.IPinnedStackAnimationListener;
 import com.android.systemui.shared.recents.model.Task;
@@ -209,7 +209,7 @@
             };
 
     protected final RecentsOrientedState mOrientationState;
-    protected final WindowSizeStrategy mSizeStrategy;
+    protected final BaseActivityInterface mSizeStrategy;
     protected RecentsAnimationController mRecentsAnimationController;
     protected RecentsAnimationTargets mRecentsAnimationTargets;
     protected AppWindowAnimationHelper mAppWindowAnimationHelper;
@@ -381,7 +381,7 @@
     };
 
     public RecentsView(Context context, AttributeSet attrs, int defStyleAttr,
-            WindowSizeStrategy sizeStrategy) {
+            BaseActivityInterface sizeStrategy) {
         super(context, attrs, defStyleAttr);
         setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing));
         setEnableFreeScroll(true);
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 72275c8..f1ea6bb 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -79,8 +79,13 @@
     <!-- Distance to move elements when swiping up to go home from launcher -->
     <dimen name="home_pullback_distance">28dp</dimen>
 
+    <!-- Distance to move the tasks when swiping up while the device is locked -->
+    <dimen name="device_locked_y_offset">-80dp</dimen>
+
     <!-- Overscroll Gesture -->
     <dimen name="gestures_overscroll_fling_threshold">40dp</dimen>
+    <dimen name="gestures_overscroll_active_threshold">80dp</dimen>
+    <dimen name="gestures_overscroll_finish_threshold">136dp</dimen>
 
     <!-- Tips Gesture Tutorial -->
     <dimen name="gesture_tutorial_title_margin_start_end">40dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 629a74b..0968d8e 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -304,4 +304,8 @@
     public ShelfPeekAnim getShelfPeekAnim() {
         return mShelfPeekAnim;
     }
+
+    public void setHintUserWillBeActive() {
+        addActivityFlags(ACTIVITY_STATE_USER_WILL_BE_ACTIVE);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 43328b6..f29f0ff 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -15,23 +15,34 @@
  */
 package com.android.quickstep;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+import static com.android.quickstep.SysUINavigationMode.getMode;
+import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
+
 import android.annotation.TargetApi;
 import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Build;
 import android.view.MotionEvent;
-import android.view.View;
 import android.view.animation.Interpolator;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
-import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.statehandlers.DepthController;
+import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.util.WindowBounds;
+import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.ShelfPeekAnim;
+import com.android.quickstep.util.SplitScreenBounds;
+import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
@@ -42,84 +53,234 @@
  * Utility class which abstracts out the logical differences between Launcher and RecentsActivity.
  */
 @TargetApi(Build.VERSION_CODES.P)
-public interface BaseActivityInterface<T extends BaseDraggingActivity> {
+public abstract class BaseActivityInterface<STATE_TYPE extends BaseState<STATE_TYPE>,
+        ACTIVITY_TYPE extends StatefulActivity<STATE_TYPE>> {
 
-    void onTransitionCancelled(boolean activityVisible);
+    private final PointF mTempPoint = new PointF();
+    public final boolean rotationSupportedByActivity;
 
-    int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect);
+    protected BaseActivityInterface(boolean rotationSupportedByActivity) {
+        this.rotationSupportedByActivity = rotationSupportedByActivity;
+    }
 
-    void onSwipeUpToRecentsComplete();
+    public void onTransitionCancelled(boolean activityVisible) {
+        ACTIVITY_TYPE activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
+        STATE_TYPE startState = activity.getStateManager().getRestState();
+        activity.getStateManager().goToState(startState, activityVisible);
+    }
 
-    default void onSwipeUpToHomeComplete() { }
-    void onAssistantVisibilityChanged(float visibility);
+    public abstract int getSwipeUpDestinationAndLength(
+            DeviceProfile dp, Context context, Rect outRect);
 
-    AnimationFactory prepareRecentsUI(
+    public void onSwipeUpToRecentsComplete() {
+        // Re apply state in case we did something funky during the transition.
+        ACTIVITY_TYPE activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
+        activity.getStateManager().reapplyState();
+    }
+
+    public void onSwipeUpToHomeComplete() { }
+
+    public abstract void onAssistantVisibilityChanged(float visibility);
+
+    public abstract AnimationFactory prepareRecentsUI(
             boolean activityVisible, Consumer<AnimatorPlaybackController> callback);
 
-    ActivityInitListener createActivityInitListener(Predicate<Boolean> onInitListener);
+    public abstract 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) {}
+    public void setOnDeferredActivityLaunchCallback(Runnable r) {}
 
     @Nullable
-    T getCreatedActivity();
+    public abstract ACTIVITY_TYPE getCreatedActivity();
 
     @Nullable
-    default DepthController getDepthController() {
+    public DepthController getDepthController() {
         return null;
     }
 
-    default boolean isResumed() {
-        BaseDraggingActivity activity = getCreatedActivity();
+    public final boolean isResumed() {
+        ACTIVITY_TYPE activity = getCreatedActivity();
         return activity != null && activity.hasBeenResumed();
     }
 
-    default boolean isStarted() {
-        BaseDraggingActivity activity = getCreatedActivity();
+    public final boolean isStarted() {
+        ACTIVITY_TYPE activity = getCreatedActivity();
         return activity != null && activity.isStarted();
     }
 
     @UiThread
     @Nullable
-    <T extends View> T getVisibleRecentsView();
+    public abstract <T extends RecentsView> T getVisibleRecentsView();
 
     @UiThread
-    boolean switchToRecentsIfVisible(Runnable onCompleteCallback);
+    public abstract boolean switchToRecentsIfVisible(Runnable onCompleteCallback);
 
-    Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target);
+    public abstract Rect getOverviewWindowBounds(
+            Rect homeBounds, RemoteAnimationTargetCompat target);
 
-    boolean allowMinimizeSplitScreen();
+    public abstract boolean allowMinimizeSplitScreen();
 
-    default boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
+    public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
         return true;
     }
 
     /**
      * Updates the prediction state to the overview state.
      */
-    default void updateOverviewPredictionState() {
-        // By default overview predictions are not supported
+    public void updateOverviewPredictionState() {
+        // By public overview predictions are not supported
     }
 
     /**
      * Used for containerType in {@link com.android.launcher3.logging.UserEventDispatcher}
      */
-    int getContainerType();
+    public abstract int getContainerType();
 
-    boolean isInLiveTileMode();
+    public abstract boolean isInLiveTileMode();
 
-    void onLaunchTaskFailed();
+    public abstract void onLaunchTaskFailed();
 
-    void onLaunchTaskSuccess();
+    public void onLaunchTaskSuccess() {
+        ACTIVITY_TYPE activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
+        activity.getStateManager().moveToRestState();
+    }
 
-    default void closeOverlay() { }
+    public void closeOverlay() { }
 
-    default void switchRunningTaskViewToScreenshot(ThumbnailData thumbnailData,
-            Runnable runnable) {}
+    public void switchRunningTaskViewToScreenshot(ThumbnailData thumbnailData, Runnable runnable) {
+        ACTIVITY_TYPE activity = getCreatedActivity();
+        if (activity == null) {
+            return;
+        }
+        RecentsView recentsView = activity.getOverviewPanel();
+        if (recentsView == null) {
+            if (runnable != null) {
+                runnable.run();
+            }
+            return;
+        }
+        recentsView.switchToScreenshot(thumbnailData, runnable);
+    }
 
-    interface AnimationFactory {
+    public void setHintUserWillBeActive() {}
+
+    /**
+     * Sets the expected window size in multi-window mode
+     */
+    public abstract void getMultiWindowSize(Context context, DeviceProfile dp, PointF out);
+
+    /**
+     * Calculates the taskView size for the provided device configuration
+     */
+    public final void calculateTaskSize(Context context, DeviceProfile dp, Rect outRect) {
+        calculateTaskSize(context, dp, getExtraSpace(context, dp), outRect);
+    }
+
+    protected abstract float getExtraSpace(Context context, DeviceProfile dp);
+
+    private void calculateTaskSize(
+            Context context, DeviceProfile dp, float extraVerticalSpace, Rect outRect) {
+        Resources res = context.getResources();
+        final boolean showLargeTaskSize = showOverviewActions(context);
+
+        final int paddingResId;
+        if (dp.isMultiWindowMode) {
+            paddingResId = R.dimen.multi_window_task_card_horz_space;
+        } else if (dp.isVerticalBarLayout()) {
+            paddingResId = R.dimen.landscape_task_card_horz_space;
+        } else if (showLargeTaskSize) {
+            paddingResId = R.dimen.portrait_task_card_horz_space_big_overview;
+        } else {
+            paddingResId = R.dimen.portrait_task_card_horz_space;
+        }
+        float paddingHorz = res.getDimension(paddingResId);
+        float paddingVert = showLargeTaskSize
+                ? 0 : res.getDimension(R.dimen.task_card_vert_space);
+
+        calculateTaskSizeInternal(context, dp, extraVerticalSpace, paddingHorz, paddingVert,
+                res.getDimension(R.dimen.task_thumbnail_top_margin), outRect);
+    }
+
+    private void calculateTaskSizeInternal(Context context, DeviceProfile dp,
+            float extraVerticalSpace, float paddingHorz, float paddingVert, float topIconMargin,
+            Rect outRect) {
+        float taskWidth, taskHeight;
+        Rect insets = dp.getInsets();
+        if (dp.isMultiWindowMode) {
+            WindowBounds bounds = SplitScreenBounds.INSTANCE.getSecondaryWindowBounds(context);
+            taskWidth = bounds.availableSize.x;
+            taskHeight = bounds.availableSize.y;
+        } else {
+            taskWidth = dp.availableWidthPx;
+            taskHeight = dp.availableHeightPx;
+        }
+
+        // Note this should be same as dp.availableWidthPx and dp.availableHeightPx unless
+        // we override the insets ourselves.
+        int launcherVisibleWidth = dp.widthPx - insets.left - insets.right;
+        int launcherVisibleHeight = dp.heightPx - insets.top - insets.bottom;
+
+        float availableHeight = launcherVisibleHeight
+                - topIconMargin - extraVerticalSpace - paddingVert;
+        float availableWidth = launcherVisibleWidth - paddingHorz;
+
+        float scale = Math.min(availableWidth / taskWidth, availableHeight / taskHeight);
+        float outWidth = scale * taskWidth;
+        float outHeight = scale * taskHeight;
+
+        // Center in the visible space
+        float x = insets.left + (launcherVisibleWidth - outWidth) / 2;
+        float y = insets.top + Math.max(topIconMargin,
+                (launcherVisibleHeight - extraVerticalSpace - outHeight) / 2);
+        outRect.set(Math.round(x), Math.round(y),
+                Math.round(x) + Math.round(outWidth), Math.round(y) + Math.round(outHeight));
+    }
+
+    /**
+     * Calculates the modal taskView size for the provided device configuration
+     */
+    public void calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect) {
+        float paddingHorz = context.getResources().getDimension(dp.isMultiWindowMode
+                ? R.dimen.multi_window_task_card_horz_space
+                : dp.isVerticalBarLayout()
+                        ? R.dimen.landscape_task_card_horz_space
+                        : R.dimen.portrait_modal_task_card_horz_space);
+        float extraVerticalSpace = getOverviewActionsHeight(context);
+        float paddingVert = 0;
+        float topIconMargin = 0;
+        calculateTaskSizeInternal(context, dp, extraVerticalSpace, paddingHorz, paddingVert,
+                topIconMargin, outRect);
+    }
+
+    /** Gets the space that the overview actions will take, including margins. */
+    public float getOverviewActionsHeight(Context context) {
+        Resources res = context.getResources();
+        float actionsBottomMargin = 0;
+        if (getMode(context) == Mode.THREE_BUTTONS) {
+            actionsBottomMargin = res.getDimensionPixelSize(
+                    R.dimen.overview_actions_bottom_margin_three_button);
+        } else {
+            actionsBottomMargin = res.getDimensionPixelSize(
+                    R.dimen.overview_actions_bottom_margin_gesture);
+        }
+        float overviewActionsHeight = actionsBottomMargin
+                + res.getDimensionPixelSize(R.dimen.overview_actions_height);
+        return overviewActionsHeight;
+    }
+
+    public interface AnimationFactory {
 
         default void onRemoteAnimationReceived(RemoteAnimationTargets targets) { }
 
@@ -137,4 +298,8 @@
          */
         default void setRecentsAttachedToAppWindow(boolean attached, boolean animate) { }
     }
+
+    protected static boolean showOverviewActions(Context context) {
+        return ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 9b515ae..8e14bbb 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -17,10 +17,12 @@
 
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 
+import android.annotation.TargetApi;
 import android.app.ActivityManager;
 import android.content.Intent;
+import android.os.Build;
 
-import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -32,6 +34,7 @@
  * Manages the state for an active system gesture, listens for events from the system and Launcher,
  * and fires events when the states change.
  */
+@TargetApi(Build.VERSION_CODES.R)
 public class GestureState implements RecentsAnimationCallbacks.RecentsAnimationListener {
 
     /**
@@ -106,6 +109,10 @@
     public static final int STATE_RECENTS_ANIMATION_ENDED =
             getFlagForIndex("STATE_RECENTS_ANIMATION_ENDED");
 
+    // Called when we create an overscroll window when swiping right to left on the most recent app
+    public static final int STATE_OVERSCROLL_WINDOW_CREATED =
+            getFlagForIndex("STATE_OVERSCROLL_WINDOW_CREATED");
+
     // Called when RecentsView stops scrolling and settles on a TaskView.
     public static final int STATE_RECENTS_SCROLLING_FINISHED =
             getFlagForIndex("STATE_RECENTS_SCROLLING_FINISHED");
@@ -189,7 +196,7 @@
     /**
      * @return the interface to the activity handing the UI updates for this gesture.
      */
-    public <T extends BaseDraggingActivity> BaseActivityInterface<T> getActivityInterface() {
+    public <T extends StatefulActivity<?>> BaseActivityInterface<?, T> getActivityInterface() {
         return mActivityInterface;
     }
 
diff --git a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
index 231ee72..0449d0c 100644
--- a/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
+++ b/quickstep/src/com/android/quickstep/OverviewComponentObserver.java
@@ -140,7 +140,7 @@
 
         if (!mDeviceState.isHomeDisabled() && (defaultHome == null || mIsDefaultHome)) {
             // User default home is same as out home app. Use Overview integrated in Launcher.
-            mActivityInterface = new LauncherActivityInterface();
+            mActivityInterface = LauncherActivityInterface.INSTANCE;
             mIsHomeAndOverviewSame = true;
             mOverviewIntent = mMyHomeIntent;
             mCurrentHomeIntent.setComponent(mMyHomeIntent.getComponent());
@@ -150,7 +150,7 @@
         } else {
             // The default home app is a different launcher. Use the fallback Overview instead.
 
-            mActivityInterface = new FallbackActivityInterface();
+            mActivityInterface = FallbackActivityInterface.INSTANCE;
             mIsHomeAndOverviewSame = false;
             mOverviewIntent = mFallbackIntent;
             mCurrentHomeIntent.setComponent(defaultHome);
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index a98aad1..8889560 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -64,24 +64,46 @@
     }
 
     /**
-     * Logs an event and accompanying {@link ItemInfo}
+     * Logs a {@link LauncherEvent}.
      */
+    @Override
+    public void log(LauncherEvent event) {
+        log(event, DEFAULT_INSTANCE_ID, LauncherAtom.ItemInfo.getDefaultInstance());
+    }
+
+    /**
+     * Logs an event and accompanying {@link InstanceId}.
+     */
+    @Override
+    public void log(LauncherEvent event, InstanceId instanceId) {
+        log(event, instanceId, LauncherAtom.ItemInfo.getDefaultInstance());
+    }
+
+    /**
+     * Logs an event and accompanying {@link ItemInfo}.
+     */
+    @Override
     public void log(LauncherEvent event, LauncherAtom.ItemInfo itemInfo) {
         log(event, DEFAULT_INSTANCE_ID, itemInfo);
     }
 
     /**
-     * Logs an event and accompanying {@link LauncherAtom.ItemInfo}
+     * Logs an event and accompanying {@link InstanceId} and {@link LauncherAtom.ItemInfo}.
      */
     @Override
     public void log(LauncherEvent event, InstanceId instanceId, LauncherAtom.ItemInfo itemInfo) {
         if (IS_VERBOSE) {
-            Log.d(TAG, String.format("\n%s\n%s", event.name(), itemInfo));
+            Log.d(TAG, instanceId == DEFAULT_INSTANCE_ID
+                    ? String.format("\n%s\n%s", event.name(), itemInfo)
+                    : String.format("%s(InstanceId:%s)\n%s", event.name(), instanceId, itemInfo));
         }
+
         if (!Utilities.ATLEAST_R) {
             return;
         }
-        SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_EVENT,
+
+        SysUiStatsLog.write(
+                SysUiStatsLog.LAUNCHER_EVENT,
                 SysUiStatsLog.LAUNCHER_UICHANGED__ACTION__DEFAULT_ACTION /* deprecated */,
                 SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__HOME /* TODO */,
                 SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__BACKGROUND /* TODO */,
@@ -118,6 +140,7 @@
     }
 
     private class SnapshotWorker extends BaseModelUpdateTask {
+
         @Override
         public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
             IntSparseArrayMap<FolderInfo> folders = dataModel.folders.clone();
@@ -140,6 +163,7 @@
             }
         }
     }
+
     private static void writeSnapshot(LauncherAtom.ItemInfo itemInfo) {
         if (IS_VERBOSE) {
             Log.d(TAG, "\nwriteSnapshot:" + itemInfo);
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index fa53be2..c1b276a 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -17,7 +17,6 @@
 
 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
 import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
-import static com.android.quickstep.util.WindowSizeStrategy.LAUNCHER_ACTIVITY_SIZE_STRATEGY;
 
 import android.content.Context;
 import android.graphics.Rect;
@@ -26,6 +25,7 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
+import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.SysUINavigationMode;
 
 public class LayoutUtils {
@@ -45,7 +45,7 @@
         // Track the bottom of the window.
         if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context)) {
             Rect taskSize = new Rect();
-            LAUNCHER_ACTIVITY_SIZE_STRATEGY.calculateTaskSize(context, dp, taskSize);
+            LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, taskSize);
             return (dp.heightPx - taskSize.height()) / 2;
         }
         int shelfHeight = dp.hotseatBarSizePx + dp.getInsets().bottom;
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index e7ff48f..e03f4b8 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -52,6 +52,7 @@
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.WindowBounds;
+import com.android.quickstep.BaseActivityInterface;
 
 import java.lang.annotation.Retention;
 import java.util.function.IntConsumer;
@@ -121,7 +122,7 @@
     private final ContentResolver mContentResolver;
     private final SharedPreferences mSharedPrefs;
     private final OrientationEventListener mOrientationListener;
-    private final WindowSizeStrategy mSizeStrategy;
+    private final BaseActivityInterface mSizeStrategy;
 
     private final Matrix mTmpMatrix = new Matrix();
 
@@ -133,7 +134,7 @@
      *                              is enabled
      * @see #setRotationWatcherEnabled(boolean)
      */
-    public RecentsOrientedState(Context context, WindowSizeStrategy sizeStrategy,
+    public RecentsOrientedState(Context context, BaseActivityInterface sizeStrategy,
             IntConsumer rotationChangeListener) {
         mContext = context;
         mContentResolver = context.getContentResolver();
diff --git a/quickstep/src/com/android/quickstep/util/WindowSizeStrategy.java b/quickstep/src/com/android/quickstep/util/WindowSizeStrategy.java
deleted file mode 100644
index b088ba8..0000000
--- a/quickstep/src/com/android/quickstep/util/WindowSizeStrategy.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright (C) 2020 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.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
-import static com.android.quickstep.SysUINavigationMode.getMode;
-import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
-import static com.android.quickstep.util.LayoutUtils.getDefaultSwipeHeight;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Rect;
-import android.os.Build;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.util.WindowBounds;
-import com.android.quickstep.SysUINavigationMode.Mode;
-
-/**
- * Utility class to wrap different layout behavior for Launcher and RecentsView
- * TODO: Merge is with {@link com.android.quickstep.BaseActivityInterface} once we remove the
- * state dependent members from {@link com.android.quickstep.LauncherActivityInterface}
- */
-@TargetApi(Build.VERSION_CODES.R)
-public abstract class WindowSizeStrategy {
-
-    public final boolean rotationSupportedByActivity;
-
-    private WindowSizeStrategy(boolean rotationSupportedByActivity) {
-        this.rotationSupportedByActivity = rotationSupportedByActivity;
-    }
-
-    /**
-     * Calculates the taskView size for the provided device configuration
-     */
-    public final void calculateTaskSize(Context context, DeviceProfile dp, Rect outRect) {
-        calculateTaskSize(context, dp, getExtraSpace(context, dp), outRect);
-    }
-
-    abstract float getExtraSpace(Context context, DeviceProfile dp);
-
-    private void calculateTaskSize(
-            Context context, DeviceProfile dp, float extraVerticalSpace, Rect outRect) {
-        Resources res = context.getResources();
-        final boolean showLargeTaskSize = showOverviewActions(context);
-
-        final int paddingResId;
-        if (dp.isMultiWindowMode) {
-            paddingResId = R.dimen.multi_window_task_card_horz_space;
-        } else if (dp.isVerticalBarLayout()) {
-            paddingResId = R.dimen.landscape_task_card_horz_space;
-        } else if (showLargeTaskSize) {
-            paddingResId = R.dimen.portrait_task_card_horz_space_big_overview;
-        } else {
-            paddingResId = R.dimen.portrait_task_card_horz_space;
-        }
-        float paddingHorz = res.getDimension(paddingResId);
-        float paddingVert = showLargeTaskSize
-                ? 0 : res.getDimension(R.dimen.task_card_vert_space);
-
-        calculateTaskSizeInternal(context, dp, extraVerticalSpace, paddingHorz, paddingVert,
-                res.getDimension(R.dimen.task_thumbnail_top_margin), outRect);
-    }
-
-    private void calculateTaskSizeInternal(Context context, DeviceProfile dp,
-            float extraVerticalSpace, float paddingHorz, float paddingVert, float topIconMargin,
-            Rect outRect) {
-        float taskWidth, taskHeight;
-        Rect insets = dp.getInsets();
-        if (dp.isMultiWindowMode) {
-            WindowBounds bounds = SplitScreenBounds.INSTANCE.getSecondaryWindowBounds(context);
-            taskWidth = bounds.availableSize.x;
-            taskHeight = bounds.availableSize.y;
-        } else {
-            taskWidth = dp.availableWidthPx;
-            taskHeight = dp.availableHeightPx;
-        }
-
-        // Note this should be same as dp.availableWidthPx and dp.availableHeightPx unless
-        // we override the insets ourselves.
-        int launcherVisibleWidth = dp.widthPx - insets.left - insets.right;
-        int launcherVisibleHeight = dp.heightPx - insets.top - insets.bottom;
-
-        float availableHeight = launcherVisibleHeight
-                - topIconMargin - extraVerticalSpace - paddingVert;
-        float availableWidth = launcherVisibleWidth - paddingHorz;
-
-        float scale = Math.min(availableWidth / taskWidth, availableHeight / taskHeight);
-        float outWidth = scale * taskWidth;
-        float outHeight = scale * taskHeight;
-
-        // Center in the visible space
-        float x = insets.left + (launcherVisibleWidth - outWidth) / 2;
-        float y = insets.top + Math.max(topIconMargin,
-                (launcherVisibleHeight - extraVerticalSpace - outHeight) / 2);
-        outRect.set(Math.round(x), Math.round(y),
-                Math.round(x) + Math.round(outWidth), Math.round(y) + Math.round(outHeight));
-    }
-
-    /**
-     * Calculates the modal taskView size for the provided device configuration
-     */
-    public void calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect) {
-        float paddingHorz = context.getResources().getDimension(dp.isMultiWindowMode
-                ? R.dimen.multi_window_task_card_horz_space
-                : dp.isVerticalBarLayout()
-                        ? R.dimen.landscape_task_card_horz_space
-                        : R.dimen.portrait_modal_task_card_horz_space);
-        float extraVerticalSpace = getOverviewActionsHeight(context);
-        float paddingVert = 0;
-        float topIconMargin = 0;
-        calculateTaskSizeInternal(context, dp, extraVerticalSpace, paddingHorz, paddingVert,
-                topIconMargin, outRect);
-    }
-
-    /** Gets the space that the overview actions will take, including margins. */
-    public float getOverviewActionsHeight(Context context) {
-        Resources res = context.getResources();
-        float actionsBottomMargin = 0;
-        if (getMode(context) == Mode.THREE_BUTTONS) {
-            actionsBottomMargin = res.getDimensionPixelSize(
-                R.dimen.overview_actions_bottom_margin_three_button);
-        } else {
-            actionsBottomMargin = res.getDimensionPixelSize(
-                R.dimen.overview_actions_bottom_margin_gesture);
-        }
-        float overviewActionsHeight = actionsBottomMargin
-                + res.getDimensionPixelSize(R.dimen.overview_actions_height);
-        return overviewActionsHeight;
-    }
-
-    public static final WindowSizeStrategy LAUNCHER_ACTIVITY_SIZE_STRATEGY =
-            new WindowSizeStrategy(true) {
-
-        @Override
-        float getExtraSpace(Context context, DeviceProfile dp) {
-            if (dp.isVerticalBarLayout()) {
-                return  0;
-            } else {
-                Resources res = context.getResources();
-                if (showOverviewActions(context)) {
-                    //TODO: this needs to account for the swipe gesture height and accessibility
-                    // UI when shown.
-                    return getOverviewActionsHeight(context);
-                } else {
-                    return getDefaultSwipeHeight(context, dp) + dp.workspacePageIndicatorHeight
-                            + res.getDimensionPixelSize(
-                                    R.dimen.dynamic_grid_hotseat_extra_vertical_size)
-                            + res.getDimensionPixelSize(
-                                    R.dimen.dynamic_grid_hotseat_bottom_padding);
-                }
-            }
-        }
-    };
-
-    public static final WindowSizeStrategy FALLBACK_RECENTS_SIZE_STRATEGY =
-            new WindowSizeStrategy(false) {
-
-        @Override
-        float getExtraSpace(Context context, DeviceProfile dp) {
-            return showOverviewActions(context)
-                    ? context.getResources().getDimensionPixelSize(R.dimen.overview_actions_height)
-                    : 0;
-        }
-    };
-
-    static boolean showOverviewActions(Context context) {
-        return ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context);
-    }
-}
diff --git a/res/values/config.xml b/res/values/config.xml
index 603dc91..4cbc597 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -69,6 +69,7 @@
     <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>
+    <string name="prediction_model_class" translatable="false"></string>
 
     <!-- View ID to use for QSB widget -->
     <item type="id" name="qsb_widget" />
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 7de44a3..7d80d81 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -108,9 +108,14 @@
     public static final int ACTIVITY_STATE_USER_ACTIVE = 1 << 4;
 
     /**
+     * State flag indicating if the user will be active shortly.
+     */
+    public static final int ACTIVITY_STATE_USER_WILL_BE_ACTIVE = 1 << 5;
+
+    /**
      * State flag indicating that a state transition is in progress
      */
-    public static final int ACTIVITY_STATE_TRANSITION_ACTIVE = 1 << 5;
+    public static final int ACTIVITY_STATE_TRANSITION_ACTIVE = 1 << 6;
 
     @Retention(SOURCE)
     @IntDef(
@@ -180,6 +185,7 @@
     @Override
     protected void onResume() {
         addActivityFlags(ACTIVITY_STATE_RESUMED | ACTIVITY_STATE_USER_ACTIVE);
+        removeActivityFlags(ACTIVITY_STATE_USER_WILL_BE_ACTIVE);
         super.onResume();
     }
 
diff --git a/src/com/android/launcher3/DropTarget.java b/src/com/android/launcher3/DropTarget.java
index 0b0983c..c1aed98 100644
--- a/src/com/android/launcher3/DropTarget.java
+++ b/src/com/android/launcher3/DropTarget.java
@@ -110,6 +110,18 @@
 
             return res;
         }
+
+
+        /**
+         * This is used to determine if an object is dropped at a different location than it was
+         * dragged from
+         */
+        public boolean isMoved() {
+            return dragInfo.cellX != originalDragInfo.cellX
+                    || dragInfo.cellY != originalDragInfo.cellY
+                    || dragInfo.screenId != originalDragInfo.screenId
+                    || dragInfo.container != originalDragInfo.container;
+        }
     }
 
     /**
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 14e604d..53e5274 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -129,7 +129,7 @@
         mIconCache = new IconCache(mContext, mInvariantDeviceProfile, iconCacheFileName);
         mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache);
         mModel = new LauncherModel(this, mIconCache, AppFilter.newInstance(mContext));
-        mPredictionModel = new PredictionModel(mContext);
+        mPredictionModel = PredictionModel.newInstance(mContext);
     }
 
     protected void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
diff --git a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
index 80b6a5a..a6bc6cf 100644
--- a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
@@ -60,6 +60,14 @@
     }
 
     @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (!mLauncher.isInState(LauncherState.ALL_APPS)) {
+            return false;
+        }
+        return super.onTouchEvent(ev);
+    }
+
+    @Override
     public void setInsets(Rect insets) {
         super.setInsets(insets);
         mLauncher.getAllAppsController()
diff --git a/src/com/android/launcher3/anim/SpringAnimationBuilder.java b/src/com/android/launcher3/anim/SpringAnimationBuilder.java
index 770df03..a9702b4 100644
--- a/src/com/android/launcher3/anim/SpringAnimationBuilder.java
+++ b/src/com/android/launcher3/anim/SpringAnimationBuilder.java
@@ -18,7 +18,6 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 
 import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.util.FloatProperty;
@@ -197,9 +196,9 @@
         animator.setDuration(getDuration()).setInterpolator(LINEAR);
         animator.addUpdateListener(anim ->
                 property.set(target, getInterpolatedValue(anim.getAnimatedFraction())));
-        animator.addListener(new AnimatorListenerAdapter() {
+        animator.addListener(new AnimationSuccessListener() {
             @Override
-            public void onAnimationEnd(Animator animation) {
+            public void onAnimationSuccess(Animator animation) {
                 property.set(target, mEndValue);
             }
         });
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 78d194b..6919339 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -110,6 +110,9 @@
     public static final BooleanFlag ENABLE_QUICK_CAPTURE_GESTURE = getDebugFlag(
             "ENABLE_QUICK_CAPTURE_GESTURE", true, "Swipe from right to left to quick capture");
 
+    public static final BooleanFlag ENABLE_QUICK_CAPTURE_WINDOW = getDebugFlag(
+            "ENABLE_QUICK_CAPTURE_WINDOW", false, "Use window to host quick capture");
+
     public static final BooleanFlag FORCE_LOCAL_OVERSCROLL_PLUGIN = getDebugFlag(
             "FORCE_LOCAL_OVERSCROLL_PLUGIN", false,
             "Use a launcher-provided OverscrollPlugin if available");
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 9a36b3e..f7fe535 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -18,23 +18,17 @@
 
 import static android.text.TextUtils.isEmpty;
 
-import static androidx.core.util.Preconditions.checkNotNull;
-
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
-import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
 import static com.android.launcher3.config.FeatureFlags.ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS;
 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_LABEL_UPDATED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED;
 import static com.android.launcher3.model.data.FolderInfo.FLAG_MANUAL_FOLDER_NAME;
-import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_CUSTOM;
-import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_EMPTY;
-import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_FOLDER_LABEL_STATE_UNSPECIFIED;
-import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_SUGGESTED;
 
 import static java.util.Arrays.asList;
-import static java.util.Arrays.stream;
 import static java.util.Optional.ofNullable;
 
 import android.animation.Animator;
@@ -94,12 +88,6 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.pageindicators.PageIndicatorDots;
-import com.android.launcher3.userevent.LauncherLogProto.Action;
-import com.android.launcher3.userevent.LauncherLogProto.ContainerType;
-import com.android.launcher3.userevent.LauncherLogProto.ItemType;
-import com.android.launcher3.userevent.LauncherLogProto.LauncherEvent;
-import com.android.launcher3.userevent.LauncherLogProto.Target;
-import com.android.launcher3.userevent.LauncherLogProto.Target.ToFolderLabelState;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Thunk;
@@ -111,10 +99,7 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
-import java.util.Optional;
-import java.util.OptionalInt;
 import java.util.stream.Collectors;
-import java.util.stream.IntStream;
 
 /**
  * Represents a set of icons chosen by the user or generated by the system.
@@ -213,8 +198,7 @@
     @Thunk int mScrollHintDir = SCROLL_NONE;
     @Thunk int mCurrentScrollDir = SCROLL_NONE;
 
-    private String mPreviousLabel;
-    private boolean mIsPreviousLabelSuggested;
+    private StatsLogManager mStatsLogManager;
 
     /**
      * Used to inflate the Workspace from XML.
@@ -227,10 +211,12 @@
         setAlwaysDrawnWithCacheEnabled(false);
 
         mLauncher = Launcher.getLauncher(context);
+        mStatsLogManager = StatsLogManager.newInstance(context);
         // We need this view to be focusable in touch mode so that when text editing of the folder
         // name is complete, we have something to focus on, thus hiding the cursor and giving
         // reliable behavior when clicking the text field (since it will always gain focus on click).
         setFocusableInTouchMode(true);
+
     }
 
     @Override
@@ -348,9 +334,9 @@
         if (DEBUG) {
             Log.d(TAG, "onBackKey newTitle=" + newTitle);
         }
-
-        mInfo.title = newTitle;
-        mInfo.setOption(FLAG_MANUAL_FOLDER_NAME, !getAcceptedSuggestionIndex().isPresent(),
+        mInfo.setTitle(newTitle);
+        mInfo.fromCustom = mInfo.hasOption(FLAG_MANUAL_FOLDER_NAME);
+        mInfo.setOption(FLAG_MANUAL_FOLDER_NAME, !mInfo.getAcceptedSuggestionIndex().isPresent(),
                 mLauncher.getModelWriter());
         mFolderIcon.onTitleChanged(newTitle);
         mLauncher.getModelWriter().updateItemInDatabase(mInfo);
@@ -441,8 +427,6 @@
         }
         mItemsInvalidated = true;
         mInfo.addListener(this);
-        Optional.ofNullable(mInfo.title).ifPresent(title -> mPreviousLabel = title.toString());
-        mIsPreviousLabelSuggested = !mInfo.hasOption(FLAG_MANUAL_FOLDER_NAME);
 
         if (!isEmpty(mInfo.title)) {
             mFolderName.setText(mInfo.title);
@@ -1347,10 +1331,8 @@
         if (d.stateAnnouncer != null) {
             d.stateAnnouncer.completeAction(R.string.item_moved);
         }
-        StatsLogManager.newInstance(getContext())
-                .log(StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED,
-                    d.logInstanceId,
-                    d.dragInfo.buildProto(mInfo));
+        mStatsLogManager
+                .log(LAUNCHER_ITEM_DROP_COMPLETED, d.logInstanceId, d.dragInfo.buildProto(mInfo));
     }
 
     // This is used so the item doesn't immediately appear in the folder when added. In one case
@@ -1455,7 +1437,8 @@
             if (hasFocus) {
                 startEditingFolderName();
             } else {
-                logCurrentFolderLabelState();
+                mStatsLogManager.log(LAUNCHER_FOLDER_LABEL_UPDATED, mInfo.buildProto());
+                logFolderLabelState();
                 mFolderName.dispatchBackKey();
             }
         }
@@ -1654,147 +1637,14 @@
         return mContent;
     }
 
-    protected void logCurrentFolderLabelState() {
-        LauncherEvent launcherEvent = LauncherEvent.newBuilder()
-                .setAction(Action.newBuilder().setType(Action.Type.SOFT_KEYBOARD))
-                .addSrcTarget(newEditTextTargetBuilder()
-                        .setFromFolderLabelState(getFromFolderLabelState())
-                        .setToFolderLabelState(getToFolderLabelState()))
-                .addSrcTarget(newFolderTargetBuilder())
-                .addSrcTarget(newParentContainerTarget())
-                .build();
-        mLauncher.getUserEventDispatcher().logLauncherEvent(launcherEvent);
-        mPreviousLabel = mFolderName.getText().toString();
-        mIsPreviousLabelSuggested = !mInfo.hasOption(FLAG_MANUAL_FOLDER_NAME);
-    }
-
-    private Target.FromFolderLabelState getFromFolderLabelState() {
-        return mPreviousLabel == null
-                ? FROM_FOLDER_LABEL_STATE_UNSPECIFIED
-                : mPreviousLabel.isEmpty()
-                ? FROM_EMPTY
-                : mIsPreviousLabelSuggested
-                ? FROM_SUGGESTED
-                : FROM_CUSTOM;
-    }
-
-    private Target.ToFolderLabelState getToFolderLabelState() {
-        String newLabel =
-                checkNotNull(mFolderName.getText().toString(),
-                "Expected valid folder label, but found null");
-        if (newLabel.equals(mPreviousLabel)) {
-            return Target.ToFolderLabelState.UNCHANGED;
-        }
-
-        if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
-            return newLabel.isEmpty()
-                ? ToFolderLabelState.TO_EMPTY_WITH_SUGGESTIONS_DISABLED
-                : ToFolderLabelState.TO_CUSTOM_WITH_SUGGESTIONS_DISABLED;
-        }
-
-        Optional<String[]> suggestedLabels = getSuggestedLabels();
-        boolean isEmptySuggestions = suggestedLabels
-                .map(labels -> stream(labels).allMatch(TextUtils::isEmpty))
-                .orElse(true);
-        if (isEmptySuggestions) {
-            return newLabel.isEmpty()
-                ? ToFolderLabelState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS
-                : ToFolderLabelState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS;
-        }
-
-        boolean hasValidPrimary = suggestedLabels
-                .map(labels -> !isEmpty(labels[0]))
-                .orElse(false);
-        if (newLabel.isEmpty()) {
-            return hasValidPrimary ? ToFolderLabelState.TO_EMPTY_WITH_VALID_PRIMARY
-                : ToFolderLabelState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
-        }
-
-        OptionalInt accepted_suggestion_index = getAcceptedSuggestionIndex();
-        if (!accepted_suggestion_index.isPresent()) {
-            return hasValidPrimary ? ToFolderLabelState.TO_CUSTOM_WITH_VALID_PRIMARY
-                : ToFolderLabelState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
-        }
-
-        switch (accepted_suggestion_index.getAsInt()) {
-            case 0:
-                return ToFolderLabelState.TO_SUGGESTION0_WITH_VALID_PRIMARY;
-            case 1:
-                return hasValidPrimary ? ToFolderLabelState.TO_SUGGESTION1_WITH_VALID_PRIMARY
-                    : ToFolderLabelState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY;
-            case 2:
-                return hasValidPrimary ? ToFolderLabelState.TO_SUGGESTION2_WITH_VALID_PRIMARY
-                    : ToFolderLabelState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY;
-            case 3:
-                return hasValidPrimary ? ToFolderLabelState.TO_SUGGESTION3_WITH_VALID_PRIMARY
-                    : ToFolderLabelState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY;
-            default:
-                // fall through
-        }
-        return ToFolderLabelState.TO_FOLDER_LABEL_STATE_UNSPECIFIED;
-
-    }
-
-    private Optional<String[]> getSuggestedLabels() {
-        return ofNullable(mInfo)
-            .map(info -> info.suggestedFolderNames)
-            .map(
-                folderNames ->
-                    (FolderNameInfo[])
-                        folderNames.getParcelableArrayExtra(FolderInfo.EXTRA_FOLDER_SUGGESTIONS))
-            .map(
-                folderNameInfoArray ->
-                    stream(folderNameInfoArray)
-                        .filter(Objects::nonNull)
-                        .map(FolderNameInfo::getLabel)
-                        .filter(Objects::nonNull)
-                        .map(CharSequence::toString)
-                        .toArray(String[]::new));
-    }
-
-    private OptionalInt getAcceptedSuggestionIndex() {
-        String newLabel = checkNotNull(mFolderName.getText().toString(),
-                "Expected valid folder label, but found null");
-        return getSuggestedLabels()
-                .map(suggestionsArray ->
-                    IntStream.range(0, suggestionsArray.length)
-                        .filter(
-                            index -> !isEmpty(suggestionsArray[index])
-                                && newLabel.equalsIgnoreCase(suggestionsArray[index]))
-                        .sequential()
-                        .findFirst()
-                ).orElse(OptionalInt.empty());
-
-    }
-
-
-    private Target.Builder newEditTextTargetBuilder() {
-        return Target.newBuilder().setType(Target.Type.ITEM).setItemType(ItemType.EDITTEXT);
-    }
-
-    private Target.Builder newFolderTargetBuilder() {
-        return Target.newBuilder()
-                .setType(Target.Type.CONTAINER)
-                .setContainerType(ContainerType.FOLDER)
-                .setPageIndex(mInfo.screenId)
-                .setGridX(mInfo.cellX)
-                .setGridY(mInfo.cellY)
-                .setCardinality(mInfo.contents.size());
-    }
-
-    private Target.Builder newParentContainerTarget() {
-        Target.Builder builder = Target.newBuilder().setType(Target.Type.CONTAINER);
-        switch (mInfo.container) {
-            case CONTAINER_HOTSEAT:
-                return builder.setContainerType(ContainerType.HOTSEAT);
-            case CONTAINER_DESKTOP:
-                return builder.setContainerType(ContainerType.WORKSPACE);
-            default:
-                throw new AssertionError(String
-                        .format("Expected container to be either %s or %s but found %s.",
-                                CONTAINER_HOTSEAT,
-                                CONTAINER_DESKTOP,
-                                mInfo.container));
-        }
+    /**
+     * Logs current folder label info.
+     *
+     * @deprecated This method is only used for log validation and soon will be removed.
+     */
+    @Deprecated
+    public void logFolderLabelState() {
+        mLauncher.getUserEventDispatcher()
+                .logLauncherEvent(mInfo.getFolderLabelStateLauncherEvent());
     }
 }
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 93208d4..153d6bc 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -20,6 +20,7 @@
 
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
 import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_LABEL_UPDATED;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -62,6 +63,8 @@
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.icons.DotRenderer;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.FolderInfo.FolderListener;
@@ -410,10 +413,10 @@
                 Executors.UI_HELPER_EXECUTOR.post(() -> {
                     d.folderNameProvider.getSuggestedFolderName(
                             getContext(), mInfo.contents, nameInfos);
-                    showFinalView(finalIndex, item, nameInfos);
+                    showFinalView(finalIndex, item, nameInfos, d.logInstanceId);
                 });
             } else {
-                showFinalView(finalIndex, item, nameInfos);
+                showFinalView(finalIndex, item, nameInfos, d.logInstanceId);
             }
         } else {
             addItem(item);
@@ -421,12 +424,12 @@
     }
 
     private void showFinalView(int finalIndex, final WorkspaceItemInfo item,
-            FolderNameInfo[] nameInfos) {
+            FolderNameInfo[] nameInfos, InstanceId instanceId) {
         postDelayed(() -> {
             mPreviewItemManager.hidePreviewItem(finalIndex, false);
             mFolder.showItem(item);
-            setLabelSuggestion(nameInfos);
-            mFolder.logCurrentFolderLabelState();
+            setLabelSuggestion(nameInfos, instanceId);
+            mFolder.logFolderLabelState();
             invalidate();
         }, DROP_IN_ANIMATION_DURATION);
     }
@@ -434,7 +437,7 @@
     /**
      * Set the suggested folder name.
      */
-    public void setLabelSuggestion(FolderNameInfo[] nameInfos) {
+    public void setLabelSuggestion(FolderNameInfo[] nameInfos, InstanceId instanceId) {
         if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
             return;
         }
@@ -445,7 +448,9 @@
         if (nameInfos == null || nameInfos[0] == null || isEmpty(nameInfos[0].getLabel())) {
             return;
         }
-        mInfo.title = nameInfos[0].getLabel();
+        mInfo.setTitle(nameInfos[0].getLabel());
+        StatsLogManager.newInstance(getContext())
+                .log(LAUNCHER_FOLDER_LABEL_UPDATED, instanceId, mInfo.buildProto());
         onTitleChanged(mInfo.title);
         mFolder.mFolderName.setText(mInfo.title);
         mFolder.mLauncher.getModelWriter().updateItemInDatabase(mInfo);
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index c62f308..350f221 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -81,10 +81,12 @@
             binderDied();
         }
 
+        SurfaceControlViewHost.SurfacePackage surfacePackage;
         try {
             mSurfaceControlViewHost = MAIN_EXECUTOR
                     .submit(() -> new SurfaceControlViewHost(mContext, mDisplay, mHostToken))
                     .get(5, TimeUnit.SECONDS);
+            surfacePackage = mSurfaceControlViewHost.getSurfacePackage();
             mHostToken.linkToDeath(this, 0);
         } catch (Exception e) {
             e.printStackTrace();
@@ -92,6 +94,14 @@
         }
 
         MAIN_EXECUTOR.execute(() -> {
+            // If mSurfaceControlViewHost is null due to any reason (e.g. binder died,
+            // happening when user leaves the preview screen before preview rendering finishes),
+            // we should return here.
+            SurfaceControlViewHost host = mSurfaceControlViewHost;
+            if (host == null) {
+                return;
+            }
+
             View view = new LauncherPreviewRenderer(mContext, mIdp).getRenderedView();
             // This aspect scales the view to fit in the surface and centers it
             final float scale = Math.min(mWidth / (float) view.getMeasuredWidth(),
@@ -107,14 +117,14 @@
                     .setInterpolator(new AccelerateDecelerateInterpolator())
                     .setDuration(FADE_IN_ANIMATION_DURATION)
                     .start();
-            mSurfaceControlViewHost.setView(view, view.getMeasuredWidth(),
+            host.setView(view, view.getMeasuredWidth(),
                     view.getMeasuredHeight());
         });
 
         Bundle result = new Bundle();
-        result.putParcelable(KEY_SURFACE_PACKAGE, mSurfaceControlViewHost.getSurfacePackage());
+        result.putParcelable(KEY_SURFACE_PACKAGE, surfacePackage);
 
-        Handler handler = new Handler(Looper.getMainLooper(), Loopermessage -> {
+        Handler handler = new Handler(Looper.getMainLooper(), message -> {
             binderDied();
             return true;
         });
@@ -128,8 +138,10 @@
     @Override
     public void binderDied() {
         if (mSurfaceControlViewHost != null) {
-            mSurfaceControlViewHost.release();
-            mSurfaceControlViewHost = null;
+            MAIN_EXECUTOR.execute(() -> {
+                mSurfaceControlViewHost.release();
+                mSurfaceControlViewHost = null;
+            });
         }
         mHostToken.unlinkToDeath(this, 0);
     }
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 9455bd3..b240f0b 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -16,10 +16,8 @@
 package com.android.launcher3.logging;
 
 import android.content.Context;
-import android.util.Log;
 
 import com.android.launcher3.R;
-import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logger.LauncherAtom.ItemInfo;
 import com.android.launcher3.logging.StatsLogUtils.LogStateProvider;
 import com.android.launcher3.util.ResourceBasedOverride;
@@ -32,8 +30,6 @@
  */
 public class StatsLogManager implements ResourceBasedOverride {
 
-    private static final String TAG = "StatsLogManager";
-
     interface EventEnum {
         int getId();
     }
@@ -61,6 +57,10 @@
                 + "resulting in a new folder creation")
         LAUNCHER_ITEM_DROP_FOLDER_CREATED(386),
 
+        @LauncherUiEvent(doc = "User action resulted in or manually updated the folder label to "
+                + "new/same value.")
+        LAUNCHER_FOLDER_LABEL_UPDATED(460),
+
         @LauncherUiEvent(doc = "A dragged item is dropped on 'Remove' button in the target bar")
         LAUNCHER_ITEM_DROPPED_ON_REMOVE(465),
 
@@ -84,9 +84,11 @@
         // ADD MORE
 
         private final int mId;
+
         LauncherEvent(int id) {
             mId = id;
         }
+
         public int getId() {
             return mId;
         }
@@ -109,22 +111,32 @@
     }
 
     /**
-     * Logs an event and accompanying {@link ItemInfo}
+     * Logs a {@link LauncherEvent}.
      */
-    public void log(LauncherEvent event, InstanceId instanceId) {
-        Log.d(TAG, String.format("%s(InstanceId:%s)", event.name(), instanceId));
-        // Call StatsLog method
+    public void log(LauncherEvent event) {
     }
 
     /**
-     * Logs an event and accompanying {@link LauncherAtom.ItemInfo}
+     * Logs an event and accompanying {@link InstanceId}.
      */
-    public void log(LauncherEvent event, InstanceId instanceId, LauncherAtom.ItemInfo itemInfo) { }
-    public void log(LauncherEvent event, LauncherAtom.ItemInfo itemInfo) { }
+    public void log(LauncherEvent event, InstanceId instanceId) {
+    }
 
+    /**
+     * Logs an event and accompanying {@link ItemInfo}.
+     */
+    public void log(LauncherEvent event, ItemInfo itemInfo) {
+    }
+
+    /**
+     * Logs an event and accompanying {@link InstanceId} and {@link ItemInfo}.
+     */
+    public void log(LauncherEvent event, InstanceId instanceId, ItemInfo itemInfo) {
+    }
 
     /**
      * Logs snapshot, or impression of the current workspace.
      */
-    public void logSnapshot() { }
+    public void logSnapshot() {
+    }
 }
diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java
index 1465100..ab921ea 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -253,8 +253,8 @@
         }
 
         private void bindPredictedItems(IntArray ranks, final Executor executor) {
-            executeCallbacksTask(
-                    c -> c.bindPredictedItems(mBgDataModel.cachedPredictedItems, ranks), executor);
+            ArrayList<AppInfo> items = new ArrayList<>(mBgDataModel.cachedPredictedItems);
+            executeCallbacksTask(c -> c.bindPredictedItems(items, ranks), executor);
         }
 
         protected void executeCallbacksTask(CallbackTask task, Executor executor) {
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 9e6282e..d05d70b 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -45,6 +45,8 @@
 import android.util.MutableInt;
 import android.util.TimingLogger;
 
+import androidx.annotation.WorkerThread;
+
 import com.android.launcher3.InstallShortcutReceiver;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
@@ -850,12 +852,11 @@
         }
     }
 
-    private List<AppInfo> loadCachedPredictions() {
+    @WorkerThread
+    private void loadCachedPredictions() {
         synchronized (mBgDataModel) {
             List<ComponentKey> componentKeys =
                     mApp.getPredictionModel().getPredictionComponentKeys();
-            List<AppInfo> results = new ArrayList<>();
-            if (componentKeys == null) return results;
             List<LauncherActivityInfo> l;
             mBgDataModel.cachedPredictedItems.clear();
             for (ComponentKey key : componentKeys) {
@@ -866,7 +867,6 @@
                 mBgDataModel.cachedPredictedItems.add(info);
                 mIconCache.getTitleAndIcon(info, false);
             }
-            return results;
         }
     }
 
diff --git a/src/com/android/launcher3/model/PredictionModel.java b/src/com/android/launcher3/model/PredictionModel.java
index 6aa41eb..f8140eb 100644
--- a/src/com/android/launcher3/model/PredictionModel.java
+++ b/src/com/android/launcher3/model/PredictionModel.java
@@ -14,60 +14,86 @@
  * limitations under the License.
  */
 package com.android.launcher3.model;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.os.UserHandle;
 
+import androidx.annotation.AnyThread;
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.util.ResourceBasedOverride;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.stream.Collectors;
 
 /**
- * Model helper for app predictions in workspace
+ * Model Helper for app predictions
  */
-public class PredictionModel {
+public class PredictionModel implements ResourceBasedOverride {
+
     private static final String CACHED_ITEMS_KEY = "predicted_item_keys";
     private static final int MAX_CACHE_ITEMS = 5;
 
-    private final Context mContext;
-    private final SharedPreferences mDevicePrefs;
+    protected Context mContext;
     private ArrayList<ComponentKey> mCachedComponentKeys;
+    private SharedPreferences mDevicePrefs;
+    private UserCache mUserCache;
 
-    public PredictionModel(Context context) {
-        mContext = context;
-        mDevicePrefs = Utilities.getDevicePrefs(mContext);
+
+    /**
+     * Retrieve instance of this object that can be overridden in runtime based on the build
+     * variant of the application.
+     */
+    public static PredictionModel newInstance(Context context) {
+        PredictionModel model = Overrides.getObject(PredictionModel.class, context,
+                R.string.prediction_model_class);
+        model.init(context);
+        return model;
     }
 
+    protected void init(Context context) {
+        mContext = context;
+        mDevicePrefs = Utilities.getDevicePrefs(mContext);
+        mUserCache = UserCache.INSTANCE.get(mContext);
+
+    }
     /**
      * Formats and stores a list of component key in device preferences.
      */
+    @AnyThread
     public void cachePredictionComponentKeys(List<ComponentKey> componentKeys) {
-        StringBuilder builder = new StringBuilder();
-        int count = Math.min(componentKeys.size(), MAX_CACHE_ITEMS);
-        for (int i = 0; i < count; i++) {
-            builder.append(componentKeys.get(i));
-            builder.append("\n");
-        }
-        mDevicePrefs.edit().putString(CACHED_ITEMS_KEY, builder.toString()).apply();
-        mCachedComponentKeys = null;
+        MODEL_EXECUTOR.execute(() -> {
+            StringBuilder builder = new StringBuilder();
+            int count = Math.min(componentKeys.size(), MAX_CACHE_ITEMS);
+            for (int i = 0; i < count; i++) {
+                builder.append(serializeComponentKeyToString(componentKeys.get(i)));
+                builder.append("\n");
+            }
+            mDevicePrefs.edit().putString(CACHED_ITEMS_KEY, builder.toString()).apply();
+            mCachedComponentKeys = null;
+        });
     }
 
     /**
      * parses and returns ComponentKeys saved by
      * {@link PredictionModel#cachePredictionComponentKeys(List)}
      */
+    @WorkerThread
     public List<ComponentKey> getPredictionComponentKeys() {
+        Preconditions.assertWorkerThread();
         if (mCachedComponentKeys == null) {
             mCachedComponentKeys = new ArrayList<>();
-
             String cachedBlob = mDevicePrefs.getString(CACHED_ITEMS_KEY, "");
             for (String line : cachedBlob.split("\n")) {
-                ComponentKey key = ComponentKey.fromString(line);
+                ComponentKey key = getComponentKeyFromSerializedString(line);
                 if (key != null) {
                     mCachedComponentKeys.add(key);
                 }
@@ -76,18 +102,26 @@
         return mCachedComponentKeys;
     }
 
-    /**
-     * Remove uninstalled applications from model
-     */
-    public void removePackage(String pkgName, UserHandle user, ArrayList<AppInfo> ids) {
-        for (int i = ids.size() - 1; i >= 0; i--) {
-            AppInfo info = ids.get(i);
-            if (info.user.equals(user) && pkgName.equals(info.componentName.getPackageName())) {
-                ids.remove(i);
-            }
+    private String serializeComponentKeyToString(ComponentKey componentKey) {
+        long userSerialNumber = mUserCache.getSerialNumberForUser(componentKey.user);
+        return componentKey.componentName.flattenToString() + "#" + userSerialNumber;
+    }
+
+    private ComponentKey getComponentKeyFromSerializedString(String str) {
+        int sep = str.indexOf('#');
+        if (sep < 0 || (sep + 1) >= str.length()) {
+            return null;
         }
-        cachePredictionComponentKeys(getPredictionComponentKeys().stream()
-                .filter(cn -> !(cn.user.equals(user) && cn.componentName.getPackageName().equals(
-                        pkgName))).collect(Collectors.toList()));
+        ComponentName componentName = ComponentName.unflattenFromString(str.substring(0, sep));
+        if (componentName == null) {
+            return null;
+        }
+        try {
+            long serialNumber = Long.parseLong(str.substring(sep + 1));
+            UserHandle userHandle = mUserCache.getUserForSerialNumber(serialNumber);
+            return userHandle != null ? new ComponentKey(componentName, userHandle) : null;
+        } catch (NumberFormatException ex) {
+            return null;
+        }
     }
 }
diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java
index 3ac6a22..096743a 100644
--- a/src/com/android/launcher3/model/data/FolderInfo.java
+++ b/src/com/android/launcher3/model/data/FolderInfo.java
@@ -16,16 +16,45 @@
 
 package com.android.launcher3.model.data;
 
+import static android.text.TextUtils.isEmpty;
+
+import static androidx.core.util.Preconditions.checkNotNull;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_CUSTOM;
+import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_EMPTY;
+import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_FOLDER_LABEL_STATE_UNSPECIFIED;
+import static com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState.FROM_SUGGESTED;
+
+import static java.util.Arrays.stream;
+import static java.util.Optional.ofNullable;
+
 import android.content.Intent;
 import android.os.Process;
+import android.text.TextUtils;
 
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.folder.FolderNameInfo;
 import com.android.launcher3.logger.LauncherAtom;
+import com.android.launcher3.logger.LauncherAtom.FromState;
+import com.android.launcher3.logger.LauncherAtom.ToState;
 import com.android.launcher3.model.ModelWriter;
+import com.android.launcher3.userevent.LauncherLogProto;
+import com.android.launcher3.userevent.LauncherLogProto.Target;
+import com.android.launcher3.userevent.LauncherLogProto.Target.FromFolderLabelState;
+import com.android.launcher3.userevent.LauncherLogProto.Target.ToFolderLabelState;
 import com.android.launcher3.util.ContentWriter;
 
 import java.util.ArrayList;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.StringJoiner;
+import java.util.stream.IntStream;
+
 
 /**
  * Represents a folder containing shortcuts or apps.
@@ -57,6 +86,20 @@
 
     public Intent suggestedFolderNames;
 
+    // Represents the title before current.
+    // Primarily used for logging purpose.
+    private CharSequence mPreviousTitle;
+
+    // True if the title before was manually entered, suggested otherwise.
+    // Primarily used for logging purpose.
+    public boolean fromCustom;
+
+    /**
+     * Used for separating {@link #mPreviousTitle} and {@link #title} when concatenating them
+     * for logging.
+     */
+    private static final CharSequence FOLDER_LABEL_DELIMITER = "=>";
+
     /**
      * The apps and shortcuts
      */
@@ -160,9 +203,20 @@
     @Override
     public LauncherAtom.ItemInfo buildProto(FolderInfo fInfo) {
         return getDefaultItemInfoBuilder()
-            .setFolderIcon(LauncherAtom.FolderIcon.newBuilder().setCardinality(contents.size()))
-            .setContainerInfo(getContainerInfo())
-            .build();
+                .setFolderIcon(LauncherAtom.FolderIcon.newBuilder().setCardinality(contents.size()))
+                .setRank(rank)
+                .setContainerInfo(getContainerInfo())
+                .build();
+    }
+
+    @Override
+    public void setTitle(CharSequence title) {
+        mPreviousTitle = this.title;
+        this.title = title;
+    }
+
+    public CharSequence getPreviousTitle() {
+        return mPreviousTitle;
     }
 
     @Override
@@ -172,4 +226,244 @@
         folderInfo.contents = this.contents;
         return folderInfo;
     }
+
+    /**
+     * Returns {@link LauncherAtom.FolderIcon} wrapped as {@link LauncherAtom.ItemInfo} for logging.
+     */
+    @Override
+    public LauncherAtom.ItemInfo buildProto() {
+        FromState fromFolderLabelState = getFromFolderLabelState();
+        ToState toFolderLabelState = getToFolderLabelState();
+        LauncherAtom.FolderIcon.Builder folderIconBuilder = LauncherAtom.FolderIcon.newBuilder()
+                .setCardinality(contents.size())
+                .setFromLabelState(fromFolderLabelState)
+                .setToLabelState(toFolderLabelState);
+
+        // If the folder label is suggested, it is logged to improve prediction model.
+        // When both old and new labels are logged together delimiter is used.
+        StringJoiner labelInfoBuilder = new StringJoiner(FOLDER_LABEL_DELIMITER);
+        if (fromFolderLabelState.equals(FromState.FROM_SUGGESTED)) {
+            labelInfoBuilder.add(mPreviousTitle);
+        }
+        if (toFolderLabelState.toString().startsWith("TO_SUGGESTION")) {
+            labelInfoBuilder.add(title);
+        }
+        if (labelInfoBuilder.length() > 0) {
+            folderIconBuilder.setLabelInfo(labelInfoBuilder.toString());
+        }
+
+        return getDefaultItemInfoBuilder()
+                .setFolderIcon(folderIconBuilder)
+                .setContainerInfo(getContainerInfo())
+                .build();
+    }
+
+    /**
+     * Returns index of the accepted suggestion.
+     */
+    public OptionalInt getAcceptedSuggestionIndex() {
+        String newLabel = checkNotNull(title,
+                "Expected valid folder label, but found null").toString();
+        return getSuggestedLabels()
+                .map(suggestionsArray ->
+                        IntStream.range(0, suggestionsArray.length)
+                                .filter(
+                                        index -> !isEmpty(suggestionsArray[index])
+                                                && newLabel.equalsIgnoreCase(
+                                                suggestionsArray[index]))
+                                .sequential()
+                                .findFirst()
+                ).orElse(OptionalInt.empty());
+
+    }
+
+    private LauncherAtom.ToState getToFolderLabelState() {
+        if (title == null) {
+            return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
+        }
+
+        if (title.equals(mPreviousTitle)) {
+            return LauncherAtom.ToState.UNCHANGED;
+        }
+
+        if (!FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+            return title.length() > 0
+                    ? LauncherAtom.ToState.TO_CUSTOM_WITH_SUGGESTIONS_DISABLED
+                    : LauncherAtom.ToState.TO_EMPTY_WITH_SUGGESTIONS_DISABLED;
+        }
+
+        Optional<String[]> suggestedLabels = getSuggestedLabels();
+        boolean isEmptySuggestions = suggestedLabels
+                .map(labels -> stream(labels).allMatch(TextUtils::isEmpty))
+                .orElse(true);
+        if (isEmptySuggestions) {
+            return title.length() > 0
+                    ? LauncherAtom.ToState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS
+                    : LauncherAtom.ToState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS;
+        }
+
+        boolean hasValidPrimary = suggestedLabels
+                .map(labels -> !isEmpty(labels[0]))
+                .orElse(false);
+        if (title.length() == 0) {
+            return hasValidPrimary ? LauncherAtom.ToState.TO_EMPTY_WITH_VALID_PRIMARY
+                    : LauncherAtom.ToState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
+        }
+
+        OptionalInt accepted_suggestion_index = getAcceptedSuggestionIndex();
+        if (!accepted_suggestion_index.isPresent()) {
+            return hasValidPrimary ? LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_PRIMARY
+                    : LauncherAtom.ToState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
+        }
+
+        switch (accepted_suggestion_index.getAsInt()) {
+            case 0:
+                return LauncherAtom.ToState.TO_SUGGESTION0;
+            case 1:
+                return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION1_WITH_VALID_PRIMARY
+                        : LauncherAtom.ToState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY;
+            case 2:
+                return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION2_WITH_VALID_PRIMARY
+                        : LauncherAtom.ToState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY;
+            case 3:
+                return hasValidPrimary ? LauncherAtom.ToState.TO_SUGGESTION3_WITH_VALID_PRIMARY
+                        : LauncherAtom.ToState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY;
+            default:
+                // fall through
+        }
+        return LauncherAtom.ToState.TO_STATE_UNSPECIFIED;
+
+    }
+
+    private LauncherAtom.FromState getFromFolderLabelState() {
+        return mPreviousTitle == null
+                ? LauncherAtom.FromState.FROM_STATE_UNSPECIFIED
+                : mPreviousTitle.length() == 0
+                        ? LauncherAtom.FromState.FROM_EMPTY
+                        : fromCustom
+                                ? LauncherAtom.FromState.FROM_CUSTOM
+                                : LauncherAtom.FromState.FROM_SUGGESTED;
+    }
+
+    private Optional<String[]> getSuggestedLabels() {
+        return ofNullable(suggestedFolderNames)
+                .map(folderNames ->
+                        (FolderNameInfo[])
+                                folderNames.getParcelableArrayExtra(EXTRA_FOLDER_SUGGESTIONS))
+                .map(folderNameInfoArray ->
+                        stream(folderNameInfoArray)
+                                .filter(Objects::nonNull)
+                                .map(FolderNameInfo::getLabel)
+                                .filter(Objects::nonNull)
+                                .map(CharSequence::toString)
+                                .toArray(String[]::new));
+    }
+
+    /**
+     * Returns {@link LauncherLogProto.LauncherEvent} to log current folder label info.
+     *
+     * @deprecated This method is used only for validation purpose and soon will be removed.
+     */
+    @Deprecated
+    public LauncherLogProto.LauncherEvent getFolderLabelStateLauncherEvent() {
+        return LauncherLogProto.LauncherEvent.newBuilder()
+                .setAction(LauncherLogProto.Action
+                        .newBuilder()
+                        .setType(LauncherLogProto.Action.Type.SOFT_KEYBOARD))
+                .addSrcTarget(Target
+                        .newBuilder()
+                        .setType(Target.Type.ITEM)
+                        .setItemType(LauncherLogProto.ItemType.EDITTEXT)
+                        .setFromFolderLabelState(convertFolderLabelState(getFromFolderLabelState()))
+                        .setToFolderLabelState(convertFolderLabelState(getToFolderLabelState())))
+                .addSrcTarget(Target.newBuilder()
+                        .setType(Target.Type.CONTAINER)
+                        .setContainerType(LauncherLogProto.ContainerType.FOLDER)
+                        .setPageIndex(screenId)
+                        .setGridX(cellX)
+                        .setGridY(cellY)
+                        .setCardinality(contents.size()))
+                .addSrcTarget(newParentContainerTarget())
+                .build();
+    }
+
+    /**
+     * @deprecated This method is used only for validation purpose and soon will be removed.
+     */
+    @Deprecated
+    private Target.Builder newParentContainerTarget() {
+        Target.Builder builder = Target.newBuilder().setType(Target.Type.CONTAINER);
+        switch (container) {
+            case CONTAINER_HOTSEAT:
+                return builder.setContainerType(LauncherLogProto.ContainerType.HOTSEAT);
+            case CONTAINER_DESKTOP:
+                return builder.setContainerType(LauncherLogProto.ContainerType.WORKSPACE);
+            default:
+                throw new AssertionError(String
+                        .format("Expected container to be either %s or %s but found %s.",
+                                CONTAINER_HOTSEAT,
+                                CONTAINER_DESKTOP,
+                                container));
+        }
+    }
+
+    /**
+     * @deprecated This method is used only for validation purpose and soon will be removed.
+     */
+    @Deprecated
+    private static FromFolderLabelState convertFolderLabelState(FromState fromState) {
+        switch (fromState) {
+            case FROM_EMPTY:
+                return FROM_EMPTY;
+            case FROM_SUGGESTED:
+                return FROM_SUGGESTED;
+            case FROM_CUSTOM:
+                return FROM_CUSTOM;
+            default:
+                return FROM_FOLDER_LABEL_STATE_UNSPECIFIED;
+        }
+    }
+
+    /**
+     * @deprecated This method is used only for validation purpose and soon will be removed.
+     */
+    @Deprecated
+    private static ToFolderLabelState convertFolderLabelState(ToState toState) {
+        switch (toState) {
+            case UNCHANGED:
+                return ToFolderLabelState.UNCHANGED;
+            case TO_SUGGESTION0:
+                return ToFolderLabelState.TO_SUGGESTION0_WITH_VALID_PRIMARY;
+            case TO_SUGGESTION1_WITH_VALID_PRIMARY:
+                return ToFolderLabelState.TO_SUGGESTION1_WITH_VALID_PRIMARY;
+            case TO_SUGGESTION1_WITH_EMPTY_PRIMARY:
+                return ToFolderLabelState.TO_SUGGESTION1_WITH_EMPTY_PRIMARY;
+            case TO_SUGGESTION2_WITH_VALID_PRIMARY:
+                return ToFolderLabelState.TO_SUGGESTION2_WITH_VALID_PRIMARY;
+            case TO_SUGGESTION2_WITH_EMPTY_PRIMARY:
+                return ToFolderLabelState.TO_SUGGESTION2_WITH_EMPTY_PRIMARY;
+            case TO_SUGGESTION3_WITH_VALID_PRIMARY:
+                return ToFolderLabelState.TO_SUGGESTION3_WITH_VALID_PRIMARY;
+            case TO_SUGGESTION3_WITH_EMPTY_PRIMARY:
+                return ToFolderLabelState.TO_SUGGESTION3_WITH_EMPTY_PRIMARY;
+            case TO_EMPTY_WITH_VALID_PRIMARY:
+                return ToFolderLabelState.TO_EMPTY_WITH_VALID_PRIMARY;
+            case TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY:
+                return ToFolderLabelState.TO_EMPTY_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
+            case TO_EMPTY_WITH_EMPTY_SUGGESTIONS:
+                return ToFolderLabelState.TO_EMPTY_WITH_EMPTY_SUGGESTIONS;
+            case TO_EMPTY_WITH_SUGGESTIONS_DISABLED:
+                return ToFolderLabelState.TO_EMPTY_WITH_SUGGESTIONS_DISABLED;
+            case TO_CUSTOM_WITH_VALID_PRIMARY:
+                return ToFolderLabelState.TO_CUSTOM_WITH_VALID_PRIMARY;
+            case TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY:
+                return ToFolderLabelState.TO_CUSTOM_WITH_VALID_SUGGESTIONS_AND_EMPTY_PRIMARY;
+            case TO_CUSTOM_WITH_EMPTY_SUGGESTIONS:
+                return ToFolderLabelState.TO_CUSTOM_WITH_EMPTY_SUGGESTIONS;
+            case TO_CUSTOM_WITH_SUGGESTIONS_DISABLED:
+                return ToFolderLabelState.TO_CUSTOM_WITH_SUGGESTIONS_DISABLED;
+            default:
+                return ToFolderLabelState.TO_FOLDER_LABEL_STATE_UNSPECIFIED;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index 7611ee7..f2b7e54 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -252,6 +252,13 @@
     /**
      * Creates {@link LauncherAtom.ItemInfo} with important fields and parent container info.
      */
+    public LauncherAtom.ItemInfo buildProto() {
+        return buildProto(null);
+    }
+
+    /**
+     * Creates {@link LauncherAtom.ItemInfo} with important fields and parent container info.
+     */
     public LauncherAtom.ItemInfo buildProto(FolderInfo fInfo) {
         LauncherAtom.ItemInfo.Builder itemBuilder = getDefaultItemInfoBuilder();
         Optional<ComponentName> nullableComponent = Optional.ofNullable(getTargetComponent());
@@ -345,4 +352,8 @@
         itemInfo.copyFrom(this);
         return itemInfo;
     }
+
+    public void setTitle(CharSequence title) {
+        this.title = title;
+    }
 }
diff --git a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
index 28a9193..a434d07 100644
--- a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
+++ b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
@@ -15,6 +15,8 @@
  */
 package com.android.systemui.plugins;
 
+import android.view.MotionEvent;
+
 import com.android.systemui.plugins.annotations.ProvidesInterface;
 
 /**
@@ -28,7 +30,7 @@
 public interface OverscrollPlugin extends Plugin {
 
     String ACTION = "com.android.systemui.action.PLUGIN_LAUNCHER_OVERSCROLL";
-    int VERSION = 3;
+    int VERSION = 4;
 
     String DEVICE_STATE_LOCKED = "Locked";
     String DEVICE_STATE_LAUNCHER = "Launcher";
@@ -41,33 +43,33 @@
     boolean isActive();
 
     /**
-     * Called when a touch is down and has been recognized as an overscroll gesture.
-     * A call of this method will always result in `onTouchUp` being called, and possibly
-     * `onFling` as well.
-     *
+     * Called when a touch has been recognized as an overscroll gesture.
+     * @param horizontalDistancePx Horizontal distance from the last finger location to the finger
+     *                               location when it first touched the screen.
+     * @param verticalDistancePx Horizontal distance from the last finger location to the finger
+     *                             location when it first touched the screen.
+     * @param thresholdPx Minimum distance for gesture.
+     * @param flingDistanceThresholdPx Minimum distance for gesture by fling.
+     * @param flingVelocityThresholdPx Minimum velocity for gesture by fling.
      * @param deviceState String representing the current device state
      * @param underlyingActivity String representing the currently active Activity
      */
-    void onTouchStart(String deviceState, String underlyingActivity);
+    void onTouchEvent(MotionEvent event,
+                      int horizontalDistancePx,
+                      int verticalDistancePx,
+                      int thresholdPx,
+                      int flingDistanceThresholdPx,
+                      int flingVelocityThresholdPx,
+                      String deviceState,
+                      String underlyingActivity);
 
     /**
-     * Called when a touch that was previously recognized has moved.
-     *
-     * @param px distance between the position of touch on this update and the position of the
-     * touch when it was initially recognized.
+     * @return `true` if overscroll gesture handling should override all other gestures.
      */
-    void onTouchTraveled(int px);
+    boolean blockOtherGestures();
 
     /**
-     * Called when a touch that was previously recognized has ended.
-     *
-     * @param px distance between the position of touch on this update and the position of the
-     * touch when it was initially recognized.
+     * @return `true` if the overscroll gesture can pan the underlying app.
      */
-    void onTouchEnd(int px);
-
-    /**
-     * Called when the user starts Compose with a fling. `onTouchUp` will also be called.
-     */
-    void onFling(float velocity);
+    boolean allowsUnderlyingActivityOverscroll();
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 14212be..b333100 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -436,9 +436,12 @@
                 sEventChecker.finishNoWait();
             }
         }
-        // b/156287114
+
         try {
-            log("Input: " + mDevice.executeShellCommand("dumpsys input"));
+            Log.e("b/156287114", "Input:");
+            for (String line : mDevice.executeShellCommand("dumpsys input").split("\\n")) {
+                Log.d("b/156287114", line);
+            }
         } catch (IOException e) {
             e.printStackTrace();
         }