Merge "LauncherInstrumentation enables even when non-system user" into ub-launcher3-master
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
index 7dc7bfc..b891120 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -310,4 +310,9 @@
     public Class<AppsDividerView> getTypeClass() {
         return AppsDividerView.class;
     }
+
+    @Override
+    public View getFocusedChild() {
+        return null;
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
index 8a810e3..d3c4c3d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -169,6 +169,7 @@
                 mDecorationHandler.extendBounds(getChildAt(i));
             }
             mDecorationHandler.onDraw(canvas);
+            mDecorationHandler.onFocusDraw(canvas, getFocusedChild());
         }
         mFocusHelper.draw(canvas);
         super.dispatchDraw(canvas);
@@ -183,7 +184,7 @@
 
     @Override
     public boolean shouldDraw() {
-        return getVisibility() != GONE;
+        return getVisibility() == VISIBLE;
     }
 
     @Override
@@ -364,4 +365,9 @@
     public Class<PredictionRowView> getTypeClass() {
         return PredictionRowView.class;
     }
+
+    @Override
+    public View getFocusedChild() {
+        return getChildAt(0);
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java
index 721e2be..b0fba3d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java
@@ -21,6 +21,7 @@
 
 import android.app.prediction.AppTarget;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
@@ -29,6 +30,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 
@@ -43,27 +45,22 @@
 public class PredictionUpdateTask extends BaseModelUpdateTask {
 
     private final List<AppTarget> mTargets;
-    private final int mContainerId;
+    private final PredictorState mPredictorState;
 
-    PredictionUpdateTask(int containerId, List<AppTarget> targets) {
-        mContainerId = containerId;
+    PredictionUpdateTask(PredictorState predictorState, List<AppTarget> targets) {
+        mPredictorState = predictorState;
         mTargets = targets;
     }
 
     @Override
     public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
-        // TODO: persist the whole list
-        Utilities.getDevicePrefs(app.getContext()).edit()
+        Context context = app.getContext();
+
+        // TODO: remove this
+        Utilities.getDevicePrefs(context).edit()
                 .putBoolean(LAST_PREDICTION_ENABLED_STATE, !mTargets.isEmpty()).apply();
 
-        FixedContainerItems fci;
-        synchronized (dataModel) {
-            fci = dataModel.extraItems.get(mContainerId);
-            if (fci == null) {
-                return;
-            }
-        }
-
+        FixedContainerItems fci = mPredictorState.items;
         Set<UserHandle> usersForChangedShortcuts = new HashSet<>(fci.items.stream()
                 .filter(info -> info.itemType == ITEM_TYPE_DEEP_SHORTCUT)
                 .map(info -> info.user)
@@ -75,7 +72,7 @@
             ShortcutInfo si = target.getShortcutInfo();
             if (si != null) {
                 usersForChangedShortcuts.add(si.getUserHandle());
-                itemInfo = new WorkspaceItemInfo(si, app.getContext());
+                itemInfo = new WorkspaceItemInfo(si, context);
                 app.getIconCache().getShortcutIcon(itemInfo, si);
             } else {
                 String className = target.getClassName();
@@ -87,16 +84,18 @@
                 UserHandle user = target.getUser();
                 itemInfo = apps.data.stream()
                         .filter(info -> user.equals(info.user) && cn.equals(info.componentName))
-                        .map(AppInfo::makeWorkspaceItem)
+                        .map(ai -> {
+                            app.getIconCache().getTitleAndIcon(ai, false);
+                            return ai.makeWorkspaceItem();
+                        })
                         .findAny()
                         .orElseGet(() -> {
-                            LauncherActivityInfo lai = app.getContext()
-                                    .getSystemService(LauncherApps.class)
+                            LauncherActivityInfo lai = context.getSystemService(LauncherApps.class)
                                     .resolveActivity(AppInfo.makeLaunchIntent(cn), user);
                             if (lai == null) {
                                 return null;
                             }
-                            AppInfo ai = new AppInfo(app.getContext(), lai, user);
+                            AppInfo ai = new AppInfo(context, lai, user);
                             app.getIconCache().getTitleAndIcon(ai, lai, false);
                             return ai.makeWorkspaceItem();
                         });
@@ -106,12 +105,15 @@
                 }
             }
 
-            itemInfo.container = mContainerId;
+            itemInfo.container = fci.containerId;
             fci.items.add(itemInfo);
         }
 
         bindExtraContainerItems(fci);
         usersForChangedShortcuts.forEach(
                 u -> dataModel.updateShortcutPinnedState(app.getContext(), u));
+
+        // Save to disk
+        mPredictorState.storage.write(context, fci.items);
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java
index b516469..166cb6c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -17,6 +17,8 @@
 
 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
 
 import android.app.prediction.AppPredictionContext;
 import android.app.prediction.AppPredictionManager;
@@ -24,16 +26,32 @@
 import android.app.prediction.AppTarget;
 import android.app.prediction.AppTargetEvent;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
+import android.os.UserHandle;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.PersistedItemArray;
 import com.android.quickstep.logging.StatsLogCompatManager;
 
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.IntStream;
 
 /**
  * Model delegate which loads prediction items
@@ -42,10 +60,12 @@
 
     public static final String LAST_PREDICTION_ENABLED_STATE = "last_prediction_enabled_state";
 
+    private final PredictorState mAllAppsState =
+            new PredictorState(CONTAINER_PREDICTION, "all_apps_predictions");
+
     private final InvariantDeviceProfile mIDP;
     private final AppEventProducer mAppEventProducer;
 
-    private AppPredictor mAllAppsPredictor;
     private boolean mActive = false;
 
     public QuickstepModelDelegate(Context context) {
@@ -57,11 +77,15 @@
     }
 
     @Override
-    public void loadItems() {
+    public void loadItems(UserManagerState ums, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts) {
         // TODO: Implement caching and preloading
-        super.loadItems();
-        mDataModel.extraItems.put(
-                CONTAINER_PREDICTION, new FixedContainerItems(CONTAINER_PREDICTION));
+        super.loadItems(ums, pinnedShortcuts);
+
+        WorkspaceItemFactory factory =
+                new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, mIDP.numAllAppsColumns);
+        mAllAppsState.items.setItems(
+                mAllAppsState.storage.read(mApp.getContext(), factory, ums.allUsers::get));
+        mDataModel.extraItems.put(CONTAINER_PREDICTION, mAllAppsState.items);
 
         mActive = true;
         recreatePredictors();
@@ -70,8 +94,8 @@
     @Override
     public void validateData() {
         super.validateData();
-        if (mAllAppsPredictor != null) {
-            mAllAppsPredictor.requestPredictionUpdate();
+        if (mAllAppsState.predictor != null) {
+            mAllAppsState.predictor.requestPredictionUpdate();
         }
     }
 
@@ -86,10 +110,7 @@
     }
 
     private void destroyPredictors() {
-        if (mAllAppsPredictor != null) {
-            mAllAppsPredictor.destroy();
-            mAllAppsPredictor = null;
-        }
+        mAllAppsState.destroyPredictor();
     }
 
     @WorkerThread
@@ -98,7 +119,6 @@
         if (!mActive) {
             return;
         }
-
         Context context = mApp.getContext();
         AppPredictionManager apm = context.getSystemService(AppPredictionManager.class);
         if (apm == null) {
@@ -107,19 +127,23 @@
 
         int count = mIDP.numAllAppsColumns;
 
-        mAllAppsPredictor = apm.createAppPredictionSession(
+        mAllAppsState.predictor = apm.createAppPredictionSession(
                 new AppPredictionContext.Builder(context)
                         .setUiSurface("home")
                         .setPredictedTargetCount(count)
                         .build());
-        mAllAppsPredictor.registerPredictionUpdates(
-                Executors.MODEL_EXECUTOR, this::onAllAppsPredictionChanged);
-        mAllAppsPredictor.requestPredictionUpdate();
+        mAllAppsState.predictor.registerPredictionUpdates(
+                Executors.MODEL_EXECUTOR, t -> handleUpdate(mAllAppsState, t));
+        mAllAppsState.predictor.requestPredictionUpdate();
     }
 
-    private void onAllAppsPredictionChanged(List<AppTarget> targets) {
-        mApp.getModel().enqueueModelUpdateTask(
-                new PredictionUpdateTask(CONTAINER_PREDICTION, targets));
+
+    private void handleUpdate(PredictorState state, List<AppTarget> targets) {
+        if (state.setTargets(targets)) {
+            // No diff, skip
+            return;
+        }
+        mApp.getModel().enqueueModelUpdateTask(new PredictionUpdateTask(state, targets));
     }
 
     @Override
@@ -131,8 +155,119 @@
     }
 
     private void onAppTargetEvent(AppTargetEvent event) {
-        if (mAllAppsPredictor != null) {
-            mAllAppsPredictor.notifyAppTargetEvent(event);
+        if (mAllAppsState.predictor != null) {
+            mAllAppsState.predictor.notifyAppTargetEvent(event);
+        }
+    }
+
+    static class PredictorState {
+
+        public final FixedContainerItems items;
+        public final PersistedItemArray storage;
+        public AppPredictor predictor;
+
+        private List<AppTarget> mLastTargets;
+
+        PredictorState(int container, String storageName) {
+            items = new FixedContainerItems(container);
+            storage = new PersistedItemArray(storageName);
+            mLastTargets = Collections.emptyList();
+        }
+
+        public void destroyPredictor() {
+            if (predictor != null) {
+                predictor.destroy();
+                predictor = null;
+            }
+        }
+
+        /**
+         * Sets the new targets and returns true if it was different than before.
+         */
+        boolean setTargets(List<AppTarget> newTargets) {
+            List<AppTarget> oldTargets = mLastTargets;
+            mLastTargets = newTargets;
+
+            int size = oldTargets.size();
+            return size == newTargets.size() && IntStream.range(0, size)
+                    .allMatch(i -> areAppTargetsSame(oldTargets.get(i), newTargets.get(i)));
+        }
+    }
+
+    /**
+     * Compares two targets for the properties which we care about
+     */
+    private static boolean areAppTargetsSame(AppTarget t1, AppTarget t2) {
+        if (!Objects.equals(t1.getPackageName(), t2.getPackageName())
+                || !Objects.equals(t1.getUser(), t2.getUser())
+                || !Objects.equals(t1.getClassName(), t2.getClassName())) {
+            return false;
+        }
+
+        ShortcutInfo s1 = t1.getShortcutInfo();
+        ShortcutInfo s2 = t2.getShortcutInfo();
+        if (s1 != null) {
+            if (s2 == null || !Objects.equals(s1.getId(), s2.getId())) {
+                return false;
+            }
+        } else if (s2 != null) {
+            return false;
+        }
+        return true;
+    }
+
+    private static class WorkspaceItemFactory implements PersistedItemArray.ItemFactory {
+
+        private final LauncherAppState mAppState;
+        private final UserManagerState mUMS;
+        private final Map<ShortcutKey, ShortcutInfo> mPinnedShortcuts;
+        private final int mMaxCount;
+
+        private int mReadCount = 0;
+
+        protected WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums,
+                Map<ShortcutKey, ShortcutInfo> pinnedShortcuts, int maxCount) {
+            mAppState = appState;
+            mUMS = ums;
+            mPinnedShortcuts = pinnedShortcuts;
+            mMaxCount = maxCount;
+        }
+
+        @Nullable
+        @Override
+        public ItemInfo createInfo(int itemType, UserHandle user, Intent intent) {
+            if (mReadCount >= mMaxCount) {
+                return null;
+            }
+            switch (itemType) {
+                case ITEM_TYPE_APPLICATION: {
+                    LauncherActivityInfo lai = mAppState.getContext()
+                            .getSystemService(LauncherApps.class)
+                            .resolveActivity(intent, user);
+                    if (lai == null) {
+                        return null;
+                    }
+                    AppInfo info = new AppInfo(lai, user, mUMS.isUserQuiet(user));
+                    mAppState.getIconCache().getTitleAndIcon(info, lai, false);
+                    mReadCount++;
+                    return info.makeWorkspaceItem();
+                }
+                case ITEM_TYPE_DEEP_SHORTCUT: {
+                    ShortcutKey key = ShortcutKey.fromIntent(intent, user);
+                    if (key == null) {
+                        return null;
+                    }
+                    ShortcutInfo si = mPinnedShortcuts.get(key);
+                    if (si == null) {
+                        return null;
+                    }
+                    WorkspaceItemInfo wii = new WorkspaceItemInfo(si, mAppState.getContext());
+                    mAppState.getIconCache().getShortcutIcon(wii, si);
+                    mReadCount++;
+                    return wii;
+                }
+            }
+            return null;
         }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 085b9b3..5ccc1e8 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE;
 import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 import static com.android.quickstep.views.RecentsView.TASK_MODALNESS;
@@ -57,7 +58,7 @@
             mRecentsView.updateEmptyMessage();
             mRecentsView.resetTaskVisuals();
         }
-        setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, state);
+        setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, new StateAnimationConfig(), state);
         mRecentsView.setFullscreenProgress(state.getOverviewFullscreenProgress());
     }
 
@@ -75,17 +76,19 @@
                     AnimationSuccessListener.forRunnable(mRecentsView::resetTaskVisuals));
         }
 
-        setAlphas(builder, toState);
+        setAlphas(builder, config, toState);
         builder.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
                 toState.getOverviewFullscreenProgress(), LINEAR);
     }
 
-    private void setAlphas(PropertySetter propertySetter, LauncherState state) {
+    private void setAlphas(PropertySetter propertySetter, StateAnimationConfig config,
+            LauncherState state) {
         float buttonAlpha = (state.getVisibleElements(mLauncher) & OVERVIEW_BUTTONS) != 0 ? 1 : 0;
         propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
                 buttonAlpha, LINEAR);
         propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
-                MultiValueAlpha.VALUE, buttonAlpha, LINEAR);
+                MultiValueAlpha.VALUE, buttonAlpha, config.getInterpolator(
+                        ANIM_OVERVIEW_ACTIONS_FADE, LINEAR));
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index 131fcbf..daa1aad 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -25,6 +25,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW_PEEK;
 import static com.android.launcher3.WorkspaceStateTransitionAnimation.getSpringScaleAnimator;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
@@ -163,10 +164,15 @@
             config.setInterpolator(ANIM_WORKSPACE_FADE, ACCEL);
             config.setInterpolator(ANIM_ALL_APPS_FADE, ACCEL);
             config.setInterpolator(ANIM_OVERVIEW_SCALE, clampToProgress(ACCEL, 0, 0.9f));
-            config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, ACCEL);
-            config.setInterpolator(ANIM_OVERVIEW_FADE, DEACCEL_1_7);
-            Workspace workspace = mActivity.getWorkspace();
+            config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, ACCEL_DEACCEL);
 
+            if (SysUINavigationMode.getMode(mActivity) == NO_BUTTON) {
+                config.setInterpolator(ANIM_OVERVIEW_FADE, FINAL_FRAME);
+            } else {
+                config.setInterpolator(ANIM_OVERVIEW_FADE, DEACCEL_1_7);
+            }
+
+            Workspace workspace = mActivity.getWorkspace();
             // Start from a higher workspace scale, but only if we're invisible so we don't jump.
             boolean isWorkspaceVisible = workspace.getVisibility() == VISIBLE;
             if (isWorkspaceVisible) {
@@ -206,8 +212,10 @@
                 config.setInterpolator(ANIM_WORKSPACE_SCALE,
                         fromState == NORMAL ? ACCEL : OVERSHOOT_1_2);
                 config.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL);
+                config.setInterpolator(ANIM_OVERVIEW_FADE, INSTANT);
             } else {
                 config.setInterpolator(ANIM_WORKSPACE_SCALE, OVERSHOOT_1_2);
+                config.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2);
 
                 // Scale up the recents, if it is not coming from the side
                 RecentsView overview = mActivity.getOverviewPanel();
@@ -225,7 +233,6 @@
                     : OVERSHOOT_1_7;
             config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, translationInterpolator);
             config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, translationInterpolator);
-            config.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2);
         } else if (fromState == HINT_STATE && toState == NORMAL) {
             config.setInterpolator(ANIM_DEPTH, DEACCEL_3);
             if (mHintToNormalDuration == -1) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index e45fa9d..57fd11a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -19,13 +19,13 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_EDU;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
 import static com.android.launcher3.touch.AbstractStateChangeTouchController.SUCCESS_TRANSITION_PROGRESS;
-import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.animation.ValueAnimator;
@@ -45,6 +45,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.graphics.OverviewScrim;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.testing.TestProtocol;
@@ -52,7 +53,9 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.TouchController;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.AssistantUtilities;
+import com.android.quickstep.util.OverviewToHomeAnim;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 
@@ -63,6 +66,8 @@
         SingleAxisSwipeDetector.Listener {
 
     private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL_3;
+    // How much of the overview scrim we can remove during the transition.
+    private static final float OVERVIEW_TO_HOME_SCRIM_PROGRESS = 0.5f;
 
     private final Launcher mLauncher;
     private final SingleAxisSwipeDetector mSwipeDetector;
@@ -156,8 +161,13 @@
         final PendingAnimation builder = new PendingAnimation(accuracy);
         if (mStartState.overviewUi) {
             RecentsView recentsView = mLauncher.getOverviewPanel();
-            builder.setFloat(recentsView, ADJACENT_PAGE_OFFSET,
-                    -mPullbackDistance / recentsView.getPageOffsetScale(), PULLBACK_INTERPOLATOR);
+            AnimatorControllerWithResistance.createRecentsResistanceFromOverviewAnim(mLauncher,
+                    builder);
+            float endScrimAlpha = Utilities.mapRange(OVERVIEW_TO_HOME_SCRIM_PROGRESS,
+                    mStartState.getOverviewScrimAlpha(mLauncher),
+                    mEndState.getOverviewScrimAlpha(mLauncher));
+            builder.setFloat(mLauncher.getDragLayer().getOverviewScrim(),
+                    OverviewScrim.SCRIM_PROGRESS, endScrimAlpha, PULLBACK_INTERPOLATOR);
             if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
                 builder.addOnFrameCallback(recentsView::redrawLiveTile);
             }
@@ -211,8 +221,13 @@
                 recentsView.switchToScreenshot(null,
                         () -> recentsView.finishRecentsAnimation(true /* toRecents */, null));
             }
-            mLauncher.getStateManager().goToState(mEndState, true,
-                    () -> onSwipeInteractionCompleted(mEndState));
+            if (mStartState == OVERVIEW) {
+                new OverviewToHomeAnim(mLauncher, () -> onSwipeInteractionCompleted(mEndState))
+                        .animateWithVelocity(velocity);
+            } else {
+                mLauncher.getStateManager().goToState(mEndState, true,
+                        () -> onSwipeInteractionCompleted(mEndState));
+            }
             if (mStartState != mEndState) {
                 logStateChange(mStartState.containerType, logAction);
             }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
index 9316938..dbff20a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java
@@ -21,11 +21,8 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
-import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_PEEK;
 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 
-import android.animation.Animator;
-import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.graphics.PointF;
@@ -35,14 +32,15 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.graphics.OverviewScrim;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.VibratorWrapper;
-import com.android.quickstep.util.StaggeredWorkspaceAnim;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
+import com.android.quickstep.util.OverviewToHomeAnim;
 import com.android.quickstep.views.RecentsView;
 
 /**
@@ -62,10 +60,10 @@
 
     private boolean mDidTouchStartInNavBar;
     private boolean mReachedOverview;
-    private boolean mIsOverviewRehidden;
-    private boolean mIsHomeStaggeredAnimFinished;
     // The last recorded displacement before we reached overview.
     private PointF mStartDisplacement = new PointF();
+    private float mStartY;
+    private AnimatorPlaybackController mOverviewResistYAnim;
 
     // Normal to Hint animation has flag SKIP_OVERVIEW, so we update this scrim with this animator.
     private ObjectAnimator mNormalToHintOverviewScrimAnimator;
@@ -123,6 +121,7 @@
                     mToState.getOverviewScrimAlpha(mLauncher));
         }
         mReachedOverview = false;
+        mOverviewResistYAnim = null;
     }
 
     @Override
@@ -160,6 +159,9 @@
         mNormalToHintOverviewScrimAnimator = null;
         mCurrentAnimation.dispatchOnCancelWithoutCancelRunnable(() -> {
             mLauncher.getStateManager().goToState(OVERVIEW, true, () -> {
+                mOverviewResistYAnim = AnimatorControllerWithResistance
+                        .createRecentsResistanceFromOverviewAnim(mLauncher, null)
+                        .createPlaybackController();
                 mReachedOverview = true;
                 maybeSwipeInteractionToOverviewComplete();
             });
@@ -173,13 +175,6 @@
         }
     }
 
-    // Used if flinging back to home after reaching overview
-    private void maybeSwipeInteractionToHomeComplete() {
-        if (mIsHomeStaggeredAnimFinished && mIsOverviewRehidden) {
-            onSwipeInteractionCompleted(NORMAL, Touch.FLING);
-        }
-    }
-
     @Override
     protected boolean handlingOverviewAnim() {
         return mDidTouchStartInNavBar && super.handlingOverviewAnim();
@@ -193,11 +188,17 @@
         if (mMotionPauseDetector.isPaused()) {
             if (!mReachedOverview) {
                 mStartDisplacement.set(xDisplacement, yDisplacement);
+                mStartY = event.getY();
             } else {
                 mRecentsView.setTranslationX((xDisplacement - mStartDisplacement.x)
                         * OVERVIEW_MOVEMENT_FACTOR);
-                mRecentsView.setTranslationY((yDisplacement - mStartDisplacement.y)
-                        * OVERVIEW_MOVEMENT_FACTOR);
+                float yProgress = (mStartDisplacement.y - yDisplacement) / mStartY;
+                if (yProgress > 0 && mOverviewResistYAnim != null) {
+                    mOverviewResistYAnim.setPlayFraction(yProgress);
+                } else {
+                    mRecentsView.setTranslationY((yDisplacement - mStartDisplacement.y)
+                            * OVERVIEW_MOVEMENT_FACTOR);
+                }
             }
             // Stay in Overview.
             return true;
@@ -212,35 +213,8 @@
         StateManager<LauncherState> stateManager = mLauncher.getStateManager();
         boolean goToHomeInsteadOfOverview = isFling;
         if (goToHomeInsteadOfOverview) {
-            if (velocity > 0) {
-                stateManager.goToState(NORMAL, true,
-                        () -> onSwipeInteractionCompleted(NORMAL, Touch.FLING));
-            } else {
-                mIsHomeStaggeredAnimFinished = mIsOverviewRehidden = false;
-
-                StaggeredWorkspaceAnim staggeredWorkspaceAnim = new StaggeredWorkspaceAnim(
-                        mLauncher, velocity, false /* animateOverviewScrim */);
-                staggeredWorkspaceAnim.addAnimatorListener(new AnimationSuccessListener() {
-                    @Override
-                    public void onAnimationSuccess(Animator animator) {
-                        mIsHomeStaggeredAnimFinished = true;
-                        maybeSwipeInteractionToHomeComplete();
-                    }
-                }).start();
-
-                // StaggeredWorkspaceAnim doesn't animate overview, so we handle it here.
-                stateManager.cancelAnimation();
-                StateAnimationConfig config = new StateAnimationConfig();
-                config.duration = OVERVIEW.getTransitionDuration(mLauncher);
-                config.animFlags = PLAY_ATOMIC_OVERVIEW_PEEK;
-                AnimatorSet anim = stateManager.createAtomicAnimation(
-                        stateManager.getState(), NORMAL, config);
-                anim.addListener(AnimationSuccessListener.forRunnable(() -> {
-                    mIsOverviewRehidden = true;
-                    maybeSwipeInteractionToHomeComplete();
-                }));
-                anim.start();
-            }
+            new OverviewToHomeAnim(mLauncher, ()-> onSwipeInteractionCompleted(NORMAL, Touch.FLING))
+                    .animateWithVelocity(velocity);
         }
         if (mReachedOverview) {
             float distanceDp = dpiFromPx(Math.max(
@@ -256,6 +230,13 @@
                     .withEndAction(goToHomeInsteadOfOverview
                             ? null
                             : this::maybeSwipeInteractionToOverviewComplete);
+            if (!goToHomeInsteadOfOverview) {
+                // Return to normal properties for the overview state.
+                StateAnimationConfig config = new StateAnimationConfig();
+                config.duration = duration;
+                LauncherState state = mLauncher.getStateManager().getState();
+                mLauncher.getStateManager().createAtomicAnimation(state, state, config).start();
+            }
         }
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 821ada4..5c9fd47 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -22,7 +22,6 @@
 import static com.android.launcher3.LauncherState.QUICK_SWITCH;
 import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD;
 import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_5;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
@@ -47,6 +46,7 @@
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.Animator;
@@ -74,7 +74,9 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.util.VibratorWrapper;
+import com.android.quickstep.AnimatedFloat;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.AnimatorControllerWithResistance;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.util.ShelfPeekAnim;
@@ -90,16 +92,17 @@
         BothAxesSwipeDetector.Listener, MotionPauseDetector.OnMotionPauseListener {
 
     /** The minimum progress of the scale/translationY animation until drag end. */
-    private static final float Y_ANIM_MIN_PROGRESS = 0.15f;
+    private static final float Y_ANIM_MIN_PROGRESS = 0.25f;
     private static final Interpolator FADE_OUT_INTERPOLATOR = DEACCEL_5;
     private static final Interpolator TRANSLATE_OUT_INTERPOLATOR = ACCEL_0_75;
-    private static final Interpolator SCALE_DOWN_INTERPOLATOR = DEACCEL;
+    private static final Interpolator SCALE_DOWN_INTERPOLATOR = LINEAR;
 
     private final BaseQuickstepLauncher mLauncher;
     private final BothAxesSwipeDetector mSwipeDetector;
     private final ShelfPeekAnim mShelfPeekAnim;
     private final float mXRange;
     private final float mYRange;
+    private final float mMaxYProgress;
     private final MotionPauseDetector mMotionPauseDetector;
     private final float mMotionPauseMinDisplacement;
     private final LauncherRecentsView mRecentsView;
@@ -113,7 +116,7 @@
     // and the other two to set overview properties based on x and y progress.
     private AnimatorPlaybackController mNonOverviewAnim;
     private AnimatorPlaybackController mXOverviewAnim;
-    private AnimatorPlaybackController mYOverviewAnim;
+    private AnimatedFloat mYOverviewAnim;
 
     public NoButtonQuickSwitchTouchController(BaseQuickstepLauncher launcher) {
         mLauncher = launcher;
@@ -123,6 +126,7 @@
         mXRange = mLauncher.getDeviceProfile().widthPx / 2f;
         mYRange = LayoutUtils.getShelfTrackingDistance(
             mLauncher, mLauncher.getDeviceProfile(), mRecentsView.getPagedOrientationHandler());
+        mMaxYProgress = mLauncher.getDeviceProfile().heightPx / mYRange;
         mMotionPauseDetector = new MotionPauseDetector(mLauncher);
         mMotionPauseMinDisplacement = mLauncher.getResources().getDimension(
                 R.dimen.motion_pause_detector_min_displacement_from_app);
@@ -270,8 +274,18 @@
                 SCALE_DOWN_INTERPOLATOR);
         yAnim.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
                 toState.getOverviewFullscreenProgress(), SCALE_DOWN_INTERPOLATOR);
-        mYOverviewAnim = yAnim.createPlaybackController();
-        mYOverviewAnim.dispatchOnStart();
+        AnimatorPlaybackController yNormalController = yAnim.createPlaybackController();
+        AnimatorControllerWithResistance yAnimWithResistance = AnimatorControllerWithResistance
+                .createForRecents(yNormalController, mLauncher,
+                        mRecentsView.getPagedViewOrientedState(), mLauncher.getDeviceProfile(),
+                        mRecentsView, RECENTS_SCALE_PROPERTY, mRecentsView,
+                        TASK_SECONDARY_TRANSLATION);
+        mYOverviewAnim = new AnimatedFloat(() -> {
+            if (mYOverviewAnim != null) {
+                yAnimWithResistance.setProgress(mYOverviewAnim.value, mMaxYProgress);
+            }
+        });
+        yNormalController.dispatchOnStart();
     }
 
     @Override
@@ -307,7 +321,7 @@
             mXOverviewAnim.setPlayFraction(xProgress);
         }
         if (mYOverviewAnim != null) {
-            mYOverviewAnim.setPlayFraction(yProgress);
+            mYOverviewAnim.updateValue(yProgress);
         }
         return true;
     }
@@ -354,9 +368,11 @@
         } else if (verticalFling) {
             targetState = velocity.y > 0 ? QUICK_SWITCH : NORMAL;
         } else {
-            // If user isn't flinging, just snap to the closest state based on x progress.
+            // If user isn't flinging, just snap to the closest state.
             boolean passedHorizontalThreshold = mXOverviewAnim.getInterpolatedProgress() > 0.5f;
-            targetState = passedHorizontalThreshold ? QUICK_SWITCH : NORMAL;
+            boolean passedVerticalThreshold = mYOverviewAnim.value > 1f;
+            targetState = passedHorizontalThreshold && !passedVerticalThreshold
+                    ? QUICK_SWITCH : NORMAL;
         }
 
         // Animate the various components to the target state.
@@ -375,9 +391,9 @@
 
         boolean flingUpToNormal = verticalFling && velocity.y < 0 && targetState == NORMAL;
 
-        float yProgress = mYOverviewAnim.getProgressFraction();
+        float yProgress = mYOverviewAnim.value;
         float startYProgress = Utilities.boundToRange(yProgress
-                - velocity.y * getSingleFrameMs(mLauncher) / mYRange, 0f, 1f);
+                - velocity.y * getSingleFrameMs(mLauncher) / mYRange, 0f, mMaxYProgress);
         final float endYProgress;
         if (flingUpToNormal) {
             endYProgress = 1;
@@ -387,12 +403,11 @@
         } else {
             endYProgress = 0;
         }
-        long yDuration = BaseSwipeDetector.calculateDuration(velocity.y,
-                Math.abs(endYProgress - startYProgress));
-        ValueAnimator yOverviewAnim = mYOverviewAnim.getAnimationPlayer();
-        yOverviewAnim.setFloatValues(startYProgress, endYProgress);
+        float yDistanceToCover = Math.abs(endYProgress - startYProgress) * mYRange;
+        long yDuration = (long) (yDistanceToCover / Math.max(1f, Math.abs(velocity.y)));
+        ValueAnimator yOverviewAnim = mYOverviewAnim.animateToValue(startYProgress, endYProgress);
         yOverviewAnim.setDuration(yDuration);
-        mYOverviewAnim.dispatchOnStart();
+        mYOverviewAnim.updateValue(startYProgress);
 
         ValueAnimator nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
         if (flingUpToNormal && !mIsHomeScreenVisible) {
@@ -457,7 +472,7 @@
             mXOverviewAnim.getAnimationPlayer().cancel();
         }
         if (mYOverviewAnim != null) {
-            mYOverviewAnim.getAnimationPlayer().cancel();
+            mYOverviewAnim.cancelAnimation();
         }
         mShelfPeekAnim.setShelfState(ShelfAnimState.CANCEL, LINEAR, 0);
         mMotionPauseDetector.clear();
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index fce019b..3586b4f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -24,6 +24,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.animation.Interpolator;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
@@ -205,14 +206,19 @@
         long maxDuration = 2 * secondaryLayerDimension;
         int verticalFactor = orientationHandler.getTaskDragDisplacementFactor(mIsRtl);
         int secondaryTaskDimension = orientationHandler.getSecondaryDimension(mTaskBeingDragged);
+        // The interpolator controlling the most prominent visual movement. We use this to determine
+        // whether we passed SUCCESS_TRANSITION_PROGRESS.
+        final Interpolator currentInterpolator;
         if (goingUp) {
+            currentInterpolator = Interpolators.LINEAR;
             mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged,
                     true /* animateTaskView */, true /* removeTask */, maxDuration);
 
             mEndDisplacement = -secondaryTaskDimension;
         } else {
+            currentInterpolator = Interpolators.ZOOM_IN;
             mPendingAnimation = mRecentsView.createTaskLaunchAnimation(
-                    mTaskBeingDragged, maxDuration, Interpolators.ZOOM_IN);
+                    mTaskBeingDragged, maxDuration, currentInterpolator);
 
             // Since the thumbnail is what is filling the screen, based the end displacement on it.
             View thumbnailView = mTaskBeingDragged.getThumbnail();
@@ -227,6 +233,9 @@
         }
         mCurrentAnimation = mPendingAnimation.createPlaybackController()
                 .setOnCancelRunnable(this::clearState);
+        // Setting this interpolator doesn't affect the visual motion, but is used to determine
+        // whether we successfully reached the target state in onDragEnd().
+        mCurrentAnimation.getTarget().setInterpolator(currentInterpolator);
         onUserControlledAnimationCreated(mCurrentAnimation);
         mCurrentAnimation.getTarget().addListener(this);
         mCurrentAnimation.dispatchOnStart();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AbsSwipeUpHandler.java
index b6d44eb..d2fee30 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -525,6 +525,20 @@
             recentsAttachedToAppWindow = mIsShelfPeeking || mIsLikelyToStartNewTask;
         }
         mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
+
+        // Reapply window transform throughout the attach animation, as the animation affects how
+        // much the window is bound by overscroll (vs moving freely).
+        if (animate) {
+            ValueAnimator reapplyWindowTransformAnim = ValueAnimator.ofFloat(0, 1);
+            reapplyWindowTransformAnim.addUpdateListener(anim -> {
+                if (mRunningWindowAnim == null) {
+                    applyWindowTransform();
+                }
+            });
+            reapplyWindowTransformAnim.setDuration(RECENTS_ATTACH_DURATION).start();
+        } else {
+            applyWindowTransform();
+        }
     }
 
     public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) {
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 1012ab2..2baba78 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -101,24 +101,6 @@
 import java.io.PrintWriter;
 import java.util.Arrays;
 import java.util.LinkedList;
-import java.util.List;
-
-/**
- * Wrapper around a list for processing arguments.
- */
-class ArgList extends LinkedList<String> {
-    public ArgList(List<String> l) {
-        super(l);
-    }
-
-    public String peekArg() {
-        return peekFirst();
-    }
-
-    public String nextArg() {
-        return pollFirst().toLowerCase();
-    }
-}
 
 /**
  * Service connected by system-UI for handling touch interaction.
@@ -238,23 +220,6 @@
             WindowBounds wb = new WindowBounds(bounds, insets);
             MAIN_EXECUTOR.execute(() -> SplitScreenBounds.INSTANCE.setSecondaryWindowBounds(wb));
         }
-
-        /** Deprecated methods **/
-        public void onQuickStep(MotionEvent motionEvent) { }
-
-        public void onQuickScrubEnd() { }
-
-        public void onQuickScrubProgress(float progress) { }
-
-        public void onQuickScrubStart() { }
-
-        public void onPreMotionEvent(int downHitTarget) { }
-
-        public void onMotionEvent(MotionEvent ev) {
-            ev.recycle();
-        }
-
-        public void onBind(ISystemUiProxy iSystemUiProxy) { }
     };
 
     private static boolean sConnected = false;
@@ -837,10 +802,10 @@
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] rawArgs) {
         if (rawArgs.length > 0 && Utilities.IS_DEBUG_DEVICE) {
-            ArgList args = new ArgList(Arrays.asList(rawArgs));
-            switch (args.nextArg()) {
+            LinkedList<String> args = new LinkedList(Arrays.asList(rawArgs));
+            switch (args.pollFirst()) {
                 case "cmd":
-                    if (args.peekArg() == null) {
+                    if (args.peekFirst() == null) {
                         printAvailableCommands(pw);
                     } else {
                         onCommand(pw, args);
@@ -881,8 +846,8 @@
         pw.println("  clear-touch-log: Clears the touch interaction log");
     }
 
-    private void onCommand(PrintWriter pw, ArgList args) {
-        switch (args.nextArg()) {
+    private void onCommand(PrintWriter pw, LinkedList<String> args) {
+        switch (args.pollFirst()) {
             case "clear-touch-log":
                 ActiveGestureLog.INSTANCE.clear();
                 break;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/OverviewToHomeAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/OverviewToHomeAnim.java
new file mode 100644
index 0000000..d2e1ded
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/OverviewToHomeAnim.java
@@ -0,0 +1,143 @@
+/*
+ * 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.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.anim.Interpolators.FINAL_FRAME;
+import static com.android.launcher3.anim.Interpolators.INSTANT;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_COMPONENTS;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
+import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_PEEK;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.util.Log;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.quickstep.views.RecentsView;
+
+/**
+ * Runs an animation from overview to home. Currently, this animation is just a wrapper around the
+ * normal state transition, in order to keep RecentsView at the same scale and translationY that
+ * it started out at as it translates offscreen. It also scrolls RecentsView to page 0 and may play
+ * a {@link StaggeredWorkspaceAnim} if we're starting from an upward fling.
+ */
+public class OverviewToHomeAnim {
+
+    private static final String TAG = "OverviewToHomeAnim";
+
+    // Constants to specify how to scroll RecentsView to the default page if it's not already there.
+    private static final int DEFAULT_PAGE = 0;
+    private static final int PER_PAGE_SCROLL_DURATION = 150;
+    private static final int MAX_PAGE_SCROLL_DURATION = 750;
+
+    private final Launcher mLauncher;
+    private final Runnable mOnReachedHome;
+
+    // Only run mOnReachedHome when both of these are true.
+    private boolean mIsHomeStaggeredAnimFinished;
+    private boolean mIsOverviewHidden;
+
+    public OverviewToHomeAnim(Launcher launcher, Runnable onReachedHome) {
+        mLauncher = launcher;
+        mOnReachedHome = onReachedHome;
+    }
+
+    /**
+     * Starts the animation. If velocity < 0 (i.e. upwards), also plays a
+     * {@link StaggeredWorkspaceAnim}.
+     */
+    public void animateWithVelocity(float velocity) {
+        StateManager<LauncherState> stateManager = mLauncher.getStateManager();
+        LauncherState startState = stateManager.getState();
+        if (startState != OVERVIEW) {
+            Log.e(TAG, "animateFromOverviewToHome: unexpected start state " + startState);
+        }
+
+        boolean playStaggeredWorkspaceAnim = velocity < 0;
+        if (playStaggeredWorkspaceAnim) {
+            StaggeredWorkspaceAnim staggeredWorkspaceAnim = new StaggeredWorkspaceAnim(
+                    mLauncher, velocity, false /* animateOverviewScrim */);
+            staggeredWorkspaceAnim.addAnimatorListener(new AnimationSuccessListener() {
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    mIsHomeStaggeredAnimFinished = true;
+                    maybeOverviewToHomeAnimComplete();
+                }
+            }).start();
+        } else {
+            mIsHomeStaggeredAnimFinished = true;
+        }
+
+        RecentsView recentsView = mLauncher.getOverviewPanel();
+        int numPagesToScroll = recentsView.getNextPage() - DEFAULT_PAGE;
+        int scrollDuration = Math.min(MAX_PAGE_SCROLL_DURATION,
+                numPagesToScroll * PER_PAGE_SCROLL_DURATION);
+        int duration = Math.max(scrollDuration, startState.getTransitionDuration(mLauncher));
+
+        StateAnimationConfig config = new UseFirstInterpolatorStateAnimConfig();
+        config.duration = duration;
+        config.animFlags = playStaggeredWorkspaceAnim
+                // StaggeredWorkspaceAnim doesn't animate overview, so we handle it here.
+                ? PLAY_ATOMIC_OVERVIEW_PEEK
+                : ANIM_ALL_COMPONENTS;
+        config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, DEACCEL);
+        config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, FINAL_FRAME);
+        config.setInterpolator(ANIM_OVERVIEW_SCALE, FINAL_FRAME);
+        config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, INSTANT);
+        AnimatorSet anim = stateManager.createAtomicAnimation(
+                startState, NORMAL, config);
+        anim.addListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                mIsOverviewHidden = true;
+                maybeOverviewToHomeAnimComplete();
+            }
+        });
+        stateManager.cancelAnimation();
+        anim.start();
+        recentsView.snapToPage(DEFAULT_PAGE, duration);
+    }
+
+    private void maybeOverviewToHomeAnimComplete() {
+        if (mIsHomeStaggeredAnimFinished && mIsOverviewHidden) {
+            mOnReachedHome.run();
+        }
+    }
+
+    /**
+     * Wrapper around StateAnimationConfig that doesn't allow interpolators to be set if they are
+     * already set. This ensures they aren't overridden before being used.
+     */
+    private static class UseFirstInterpolatorStateAnimConfig extends StateAnimationConfig {
+        @Override
+        public void setInterpolator(int animId, Interpolator interpolator) {
+            if (mInterpolators[animId] == null || interpolator == null) {
+                super.setInterpolator(animId, interpolator);
+            }
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java
index 79d57c5..1bf2fbf 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/OverviewActionsView.java
@@ -105,6 +105,7 @@
     public OverviewActionsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr, 0);
         mMultiValueAlpha = new MultiValueAlpha(this, 4);
+        mMultiValueAlpha.setUpdateVisibility(true);
     }
 
     @Override
@@ -168,7 +169,6 @@
         }
         boolean isHidden = mHiddenFlags != 0;
         mMultiValueAlpha.getProperty(INDEX_HIDDEN_FLAGS_ALPHA).setValue(isHidden ? 0 : 1);
-        setVisibility(isHidden ? INVISIBLE : VISIBLE);
     }
 
     /**
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 ef75d8a..c4444c3 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
@@ -2354,7 +2354,14 @@
         if (pageIndex == -1) {
             return 0;
         }
-        return getScrollForPage(pageIndex) - mOrientationHandler.getPrimaryScroll(this);
+        // Unbound the scroll (due to overscroll) if the adjacent tasks are offset away from it.
+        // This allows the page to move freely, given there's no visual indication why it shouldn't.
+        int boundedScroll = mOrientationHandler.getPrimaryScroll(this);
+        int unboundedScroll = getUnboundedScroll();
+        float unboundedProgress = mAdjacentPageOffset;
+        int scroll = Math.round(unboundedScroll * unboundedProgress
+                + boundedScroll * (1 - unboundedProgress));
+        return getScrollForPage(pageIndex) - scroll;
     }
 
     public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
index 7651dd8..c5f673f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -326,22 +326,14 @@
         // Draw the background in all cases, except when the thumbnail data is opaque
         final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null
                 || mThumbnailData == null;
-        if (drawBackgroundOnly || mPreviewPositionHelper.mClipBottom > 0
-                || mThumbnailData.isTranslucent) {
+        if (drawBackgroundOnly || mThumbnailData.isTranslucent) {
             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint);
             if (drawBackgroundOnly) {
                 return;
             }
         }
 
-        if (mPreviewPositionHelper.mClipBottom > 0) {
-            canvas.save();
-            canvas.clipRect(x, y, width, mPreviewPositionHelper.mClipBottom);
-            canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
-            canvas.restore();
-        } else {
-            canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
-        }
+        canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
     }
 
     public TaskView getTaskView() {
@@ -379,7 +371,6 @@
     }
 
     private void updateThumbnailMatrix() {
-        mPreviewPositionHelper.mClipBottom = -1;
         mPreviewPositionHelper.mIsOrientationChanged = false;
         if (mBitmapShader != null && mThumbnailData != null) {
             mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(),
@@ -473,40 +464,97 @@
         /**
          * Updates the matrix based on the provided parameters
          */
-        public void updateThumbnailMatrix(Rect thumbnailPosition, ThumbnailData thumbnailData,
+        public void updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData,
                 int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation) {
             boolean isRotated = false;
             boolean isOrientationDifferent;
-            mClipBottom = -1;
 
-            float scale = thumbnailData.scale;
-            Rect activityInsets = dp.getInsets();
-            Rect thumbnailInsets = getBoundedInsets(activityInsets, thumbnailData.insets);
-            final float thumbnailWidth = thumbnailPosition.width()
-                    - (thumbnailInsets.left + thumbnailInsets.right) * scale;
-            final float thumbnailHeight = thumbnailPosition.height()
-                    - (thumbnailInsets.top + thumbnailInsets.bottom) * scale;
-
-            final float thumbnailScale;
             int thumbnailRotation = thumbnailData.rotation;
             int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
+            RectF thumbnailClipHint = new RectF(thumbnailData.insets);
+
+            float scale = thumbnailData.scale;
+            final float thumbnailScale;
 
             // Landscape vs portrait change
             boolean windowingModeSupportsRotation = !dp.isMultiWindowMode
                     && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
             isOrientationDifferent = isOrientationChange(deltaRotate)
                     && windowingModeSupportsRotation;
-            if (canvasWidth == 0) {
+            if (canvasWidth == 0 || canvasHeight == 0 || scale == 0) {
                 // If we haven't measured , skip the thumbnail drawing and only draw the background
                 // color
                 thumbnailScale = 0f;
             } else {
                 // Rotate the screenshot if not in multi-window mode
                 isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
-                // Scale the screenshot to always fit the width of the card.
-                thumbnailScale = isOrientationDifferent
-                        ? canvasWidth / thumbnailHeight
-                        : canvasWidth / thumbnailWidth;
+
+                float surfaceWidth = thumbnailBounds.width() / scale;
+                float surfaceHeight = thumbnailBounds.height() / scale;
+                float availableWidth = surfaceWidth
+                        - (thumbnailClipHint.left + thumbnailClipHint.right);
+                float availableHeight = surfaceHeight
+                        - (thumbnailClipHint.top + thumbnailClipHint.bottom);
+
+                final float targetW, targetH;
+                if (isOrientationDifferent) {
+                    targetW = canvasHeight;
+                    targetH = canvasWidth;
+                } else {
+                    targetW = canvasWidth;
+                    targetH = canvasHeight;
+                }
+                float canvasAspect = targetW / targetH;
+
+                // Update the clipHint such that
+                //   > the final clipped position has same aspect ratio as requested by canvas
+                //   > the clipped region is within the task insets if possible
+                //   > the clipped region is not scaled up when drawing. If that is not possible
+                //     while staying within the taskInsets, move outside the insets.
+                float croppedWidth = availableWidth;
+                if (croppedWidth < targetW) {
+                    croppedWidth = Math.min(targetW, surfaceWidth);
+                }
+
+                float croppedHeight = croppedWidth / canvasAspect;
+                if (croppedHeight > availableHeight) {
+                    croppedHeight = availableHeight;
+                    if (croppedHeight < targetH) {
+                        croppedHeight = Math.min(targetH, surfaceHeight);
+                    }
+                    croppedWidth = croppedHeight * canvasAspect;
+
+                    // One last check in case the task aspect radio messed up something
+                    if (croppedWidth > surfaceWidth) {
+                        croppedWidth = surfaceWidth;
+                        croppedHeight = croppedWidth / canvasAspect;
+                    }
+                }
+
+                // Update the clip hints
+                float halfExtraW = (availableWidth - croppedWidth) / 2;
+                thumbnailClipHint.left += halfExtraW;
+                thumbnailClipHint.right += halfExtraW;
+                if (thumbnailClipHint.left < 0) {
+                    thumbnailClipHint.right += thumbnailClipHint.left;
+                    thumbnailClipHint.left = 0;
+                } else if (thumbnailClipHint.right < 0) {
+                    thumbnailClipHint.left += thumbnailClipHint.right;
+                    thumbnailClipHint.right = 0;
+                }
+
+                float halfExtraH = (availableHeight - croppedHeight) / 2;
+                thumbnailClipHint.top += halfExtraH;
+                thumbnailClipHint.bottom += halfExtraH;
+                if (thumbnailClipHint.top < 0) {
+                    thumbnailClipHint.bottom += thumbnailClipHint.top;
+                    thumbnailClipHint.top = 0;
+                } else if (thumbnailClipHint.bottom < 0) {
+                    thumbnailClipHint.top += thumbnailClipHint.bottom;
+                    thumbnailClipHint.bottom = 0;
+                }
+
+                thumbnailScale = targetW / (croppedWidth * scale);
             }
 
             Rect splitScreenInsets = dp.getInsets();
@@ -516,24 +564,24 @@
                     mClippedInsets.offsetTo(splitScreenInsets.left * scale,
                             splitScreenInsets.top * scale);
                 } else {
-                    mClippedInsets.offsetTo(thumbnailInsets.left * scale,
-                            thumbnailInsets.top * scale);
+                    mClippedInsets.offsetTo(thumbnailClipHint.left * scale,
+                            thumbnailClipHint.top * scale);
                 }
                 mMatrix.setTranslate(
-                        -thumbnailInsets.left * scale,
-                        -thumbnailInsets.top * scale);
+                        -thumbnailClipHint.left * scale,
+                        -thumbnailClipHint.top * scale);
             } else {
-                setThumbnailRotation(deltaRotate, thumbnailInsets, scale, thumbnailPosition);
+                setThumbnailRotation(deltaRotate, thumbnailClipHint, scale, thumbnailBounds);
             }
 
             final float widthWithInsets;
             final float heightWithInsets;
             if (isOrientationDifferent) {
-                widthWithInsets = thumbnailPosition.height() * thumbnailScale;
-                heightWithInsets = thumbnailPosition.width() * thumbnailScale;
+                widthWithInsets = thumbnailBounds.height() * thumbnailScale;
+                heightWithInsets = thumbnailBounds.width() * thumbnailScale;
             } else {
-                widthWithInsets = thumbnailPosition.width() * thumbnailScale;
-                heightWithInsets = thumbnailPosition.height() * thumbnailScale;
+                widthWithInsets = thumbnailBounds.width() * thumbnailScale;
+                heightWithInsets = thumbnailBounds.height() * thumbnailScale;
             }
             mClippedInsets.left *= thumbnailScale;
             mClippedInsets.top *= thumbnailScale;
@@ -549,22 +597,9 @@
             }
 
             mMatrix.postScale(thumbnailScale, thumbnailScale);
-
-            float bitmapHeight = Math.max(0,
-                    (isOrientationDifferent ? thumbnailWidth : thumbnailHeight) * thumbnailScale);
-            if (Math.round(bitmapHeight) < canvasHeight) {
-                mClipBottom = bitmapHeight;
-            }
             mIsOrientationChanged = isOrientationDifferent;
         }
 
-        private Rect getBoundedInsets(Rect activityInsets, Rect insets) {
-            return new Rect(Math.min(insets.left, activityInsets.left),
-                    Math.min(insets.top, activityInsets.top),
-                    Math.min(insets.right, activityInsets.right),
-                    Math.min(insets.bottom, activityInsets.bottom));
-        }
-
         private int getRotationDelta(int oldRotation, int newRotation) {
             int delta = newRotation - oldRotation;
             if (delta < 0) delta += 4;
@@ -580,12 +615,12 @@
             return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
         }
 
-        private void setThumbnailRotation(int deltaRotate, Rect thumbnailInsets, float scale,
+        private void setThumbnailRotation(int deltaRotate, RectF thumbnailInsets, float scale,
                 Rect thumbnailPosition) {
-            int newLeftInset = 0;
-            int newTopInset = 0;
-            int translateX = 0;
-            int translateY = 0;
+            float newLeftInset = 0;
+            float newTopInset = 0;
+            float translateX = 0;
+            float translateY = 0;
 
             mMatrix.setRotate(90 * deltaRotate);
             switch (deltaRotate) { /* Counter-clockwise */
diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
index 23b02d5..a19a67c 100644
--- a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
+++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
@@ -18,6 +18,8 @@
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
+import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 
 import android.animation.TimeInterpolator;
 import android.content.Context;
@@ -27,12 +29,16 @@
 import android.graphics.RectF;
 import android.util.FloatProperty;
 
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.views.RecentsView;
 
 /**
  * Controls an animation that can go beyond progress = 1, at which point resistance should be
@@ -48,15 +54,32 @@
      */
     public static final float TWO_BUTTON_EXTRA_DRAG_FACTOR = 0.25f;
 
-    /**
-     * Start slowing down the rate of scaling down when recents view is smaller than this scale.
-     */
-    private static final float RECENTS_SCALE_START_RESIST = 0.75f;
+    private enum RecentsParams {
+        FROM_APP(0.75f, 0.5f, 1f),
+        FROM_OVERVIEW(1f, 0.75f, 0.5f);
 
-    /**
-     * Recents view will reach this scale at the very end of the drag.
-     */
-    private static final float RECENTS_SCALE_MAX_RESIST = 0.5f;
+        RecentsParams(float scaleStartResist, float scaleMaxResist, float translationFactor) {
+            this.scaleStartResist = scaleStartResist;
+            this.scaleMaxResist = scaleMaxResist;
+            this.translationFactor = translationFactor;
+        }
+
+        /**
+         * Start slowing down the rate of scaling down when recents view is smaller than this scale.
+         */
+        public final float scaleStartResist;
+
+        /**
+         * Recents view will reach this scale at the very end of the drag.
+         */
+        public final float scaleMaxResist;
+
+        /**
+         * How much translation to apply to RecentsView when the drag reaches the top of the screen,
+         * where 0 will keep it centered and 1 will have it barely touch the top of the screen.
+         */
+        public final float translationFactor;
+    }
 
     private static final TimeInterpolator RECENTS_SCALE_RESIST_INTERPOLATOR = DEACCEL;
     private static final TimeInterpolator RECENTS_TRANSLATE_RESIST_INTERPOLATOR = LINEAR;
@@ -115,6 +138,24 @@
             RecentsOrientedState recentsOrientedState, DeviceProfile dp, SCALE scaleTarget,
             FloatProperty<SCALE> scaleProperty, TRANSLATION translationTarget,
             FloatProperty<TRANSLATION> translationProperty) {
+
+        PendingAnimation resistAnim = createRecentsResistanceAnim(null, context,
+                recentsOrientedState, dp, scaleTarget, scaleProperty, translationTarget,
+                translationProperty, RecentsParams.FROM_APP);
+
+        AnimatorPlaybackController resistanceController = resistAnim.createPlaybackController();
+        return new AnimatorControllerWithResistance(normalController, resistanceController);
+    }
+
+    /**
+     * Creates the resistance animation for {@link #createForRecents}, or can be used separately
+     * when starting from recents, i.e. {@link #createRecentsResistanceFromOverviewAnim}.
+     */
+    public static <SCALE, TRANSLATION> PendingAnimation createRecentsResistanceAnim(
+            @Nullable PendingAnimation resistAnim, Context context,
+            RecentsOrientedState recentsOrientedState, DeviceProfile dp, SCALE scaleTarget,
+            FloatProperty<SCALE> scaleProperty, TRANSLATION translationTarget,
+            FloatProperty<TRANSLATION> translationProperty, RecentsParams params) {
         Rect startRect = new Rect();
         LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, startRect,
                 recentsOrientedState.getOrientationHandler());
@@ -125,7 +166,9 @@
             distanceToCover = (long)
                     ((dp.heightPx - startRect.bottom) * TWO_BUTTON_EXTRA_DRAG_FACTOR);
         }
-        PendingAnimation resistAnim = new PendingAnimation(distanceToCover * 2);
+        if (resistAnim == null) {
+            resistAnim = new PendingAnimation(distanceToCover * 2);
+        }
 
         PointF pivot = new PointF();
         float fullscreenScale = recentsOrientedState.getFullScreenScaleAndPivot(
@@ -141,9 +184,9 @@
         } else {
             // Create an interpolator that resists the scale so the scale doesn't get smaller than
             // RECENTS_SCALE_MAX_RESIST.
-            float startResist = Utilities.getProgress(RECENTS_SCALE_START_RESIST, startScale,
+            float startResist = Utilities.getProgress(params.scaleStartResist , startScale,
                     endScale);
-            float maxResist = Utilities.getProgress(RECENTS_SCALE_MAX_RESIST, startScale, endScale);
+            float maxResist = Utilities.getProgress(params.scaleMaxResist, startScale, endScale);
             scaleInterpolator = t -> {
                 if (t < startResist) {
                     return t;
@@ -160,17 +203,28 @@
             // Compute where the task view would be based on the end scale, if we didn't translate.
             RectF endRectF = new RectF(startRect);
             Matrix temp = new Matrix();
-            temp.setScale(RECENTS_SCALE_MAX_RESIST, RECENTS_SCALE_MAX_RESIST, pivot.x, pivot.y);
+            temp.setScale(params.scaleMaxResist, params.scaleMaxResist, pivot.x, pivot.y);
             temp.mapRect(endRectF);
             // Translate such that the task view touches the top of the screen when drag does.
             float endTranslation = endRectF.top * recentsOrientedState.getOrientationHandler()
-                    .getSecondaryTranslationDirectionFactor();
+                    .getSecondaryTranslationDirectionFactor() * params.translationFactor;
             resistAnim.addFloat(translationTarget, translationProperty, 0, endTranslation,
                     RECENTS_TRANSLATE_RESIST_INTERPOLATOR);
         }
 
-        AnimatorPlaybackController resistanceController = resistAnim.createPlaybackController();
-        return new AnimatorControllerWithResistance(normalController, resistanceController);
+        return resistAnim;
     }
 
+    /**
+     * Helper method to update or create a PendingAnimation suitable for animating
+     * a RecentsView interaction that started from the overview state.
+     */
+    public static PendingAnimation createRecentsResistanceFromOverviewAnim(
+            BaseDraggingActivity activity, @Nullable PendingAnimation resistanceAnim) {
+        RecentsView recentsView = activity.getOverviewPanel();
+        return createRecentsResistanceAnim(resistanceAnim, activity,
+                recentsView.getPagedViewOrientedState(), activity.getDeviceProfile(),
+                recentsView, RECENTS_SCALE_PROPERTY, recentsView, TASK_SECONDARY_TRANSLATION,
+                RecentsParams.FROM_OVERVIEW);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index 81d24d7..df9b0cf 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -71,7 +71,7 @@
 public final class RecentsOrientedState implements SharedPreferences.OnSharedPreferenceChangeListener {
 
     private static final String TAG = "RecentsOrientedState";
-    private static final boolean DEBUG = true;
+    private static final boolean DEBUG = false;
 
     private ContentObserver mSystemAutoRotateObserver = new ContentObserver(new Handler()) {
         @Override
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 9a89dee..fe9717c 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -35,7 +35,8 @@
 
     <color name="icon_background">#E0E0E0</color> <!-- Gray 300 -->
 
-    <color name="all_apps_section_fill">#327d7d7d</color>
+    <color name="all_apps_section_fill">#32c0c0c0</color>
+    <color name="all_apps_section_focused_item">#40c0c0c0</color>
 
     <color name="gesture_tutorial_ripple_color">#A0C2F9</color> <!-- Light Blue -->
     <color name="gesture_tutorial_fake_task_view_color">#6DA1FF</color> <!-- Light Blue -->
diff --git a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 0e760f9..fb08c56 100644
--- a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -51,7 +51,7 @@
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.Executors;
@@ -92,9 +92,9 @@
                 SCREEN, CELLX, CELLY, RESTORED, INTENT
         });
 
-        mLoaderCursor = new LoaderCursor(mCursor, LauncherSettings.Favorites.CONTENT_URI, mApp,
-                new UserManagerState());
-        mLoaderCursor.allUsers.put(0, Process.myUserHandle());
+        UserManagerState ums = new UserManagerState();
+        mLoaderCursor = new LoaderCursor(mCursor, Favorites.CONTENT_URI, mApp, ums);
+        ums.allUsers.put(0, Process.myUserHandle());
     }
 
     private void initCursor(int itemType, String title) {
diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java
index 432073e..f61bc05 100644
--- a/src/com/android/launcher3/AutoInstallsLayout.java
+++ b/src/com/android/launcher3/AutoInstallsLayout.java
@@ -658,7 +658,7 @@
         }
     }
 
-    protected static void beginDocument(XmlPullParser parser, String firstElementName)
+    public static void beginDocument(XmlPullParser parser, String firstElementName)
             throws XmlPullParserException, IOException {
         int type;
         while ((type = parser.next()) != XmlPullParser.START_TAG
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 2d711e6..af3722a 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -527,6 +527,22 @@
         return mViewPager == null ? getActiveRecyclerView() : mViewPager;
     }
 
+    /**
+     * Returns the ItemInfo of a view that is in focus, ready to be launched by an IME.
+     */
+    public ItemInfo getHighlightedItemInfo() {
+        View view = getFloatingHeaderView().getFocusedChild();
+        if (view != null && view.getTag() instanceof ItemInfo) {
+            return ((ItemInfo) view.getTag());
+        }
+        if (getActiveRecyclerView().getApps().getFocusedChild() != null) {
+            // TODO: when new pipelines are included, getSearchResults
+            // should be supported at recycler view level and not apps list level.
+            return getActiveRecyclerView().getApps().getFocusedChild().appInfo;
+        }
+        return null;
+    }
+
     public RecyclerViewFastScroller getScrollBar() {
         AllAppsRecyclerView rv = getActiveRecyclerView();
         return rv == null ? null : rv.getScrollbar();
@@ -658,7 +674,8 @@
             applyPadding();
             setupOverlay();
             if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
-                recyclerView.addItemDecoration(new AllAppsSectionDecorator(getApps()));
+                recyclerView.addItemDecoration(new AllAppsSectionDecorator(
+                        AllAppsContainerView.this));
             }
         }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java b/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java
index ac55072..a168c06 100644
--- a/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java
+++ b/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java
@@ -21,6 +21,7 @@
 import android.graphics.RectF;
 import android.view.View;
 
+import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.R;
@@ -34,19 +35,19 @@
  */
 public class AllAppsSectionDecorator extends RecyclerView.ItemDecoration {
 
-    private final AlphabeticalAppsList mApps;
+    private final AllAppsContainerView mAppsView;
 
-    AllAppsSectionDecorator(AlphabeticalAppsList appsList) {
-        mApps = appsList;
+    AllAppsSectionDecorator(AllAppsContainerView appsContainerView) {
+        mAppsView = appsContainerView;
     }
 
     @Override
     public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
         // Iterate through views in recylerview and draw bounds around views in the same section.
         // Since views in the same section will follow each other, we can skip to a last view in
-        // a section to get the bounds of the section without having to iterate on evert item.
+        // a section to get the bounds of the section without having to iterate on every item.
         int itemCount = parent.getChildCount();
-        List<AlphabeticalAppsList.AdapterItem> adapterItems = mApps.getAdapterItems();
+        List<AlphabeticalAppsList.AdapterItem> adapterItems = mAppsView.getApps().getAdapterItems();
         SectionDecorationHandler lastDecorationHandler = null;
         int i = 0;
         while (i < itemCount) {
@@ -69,7 +70,6 @@
                     i = endIndex;
                     continue;
                 }
-
             }
             i++;
         }
@@ -78,13 +78,21 @@
         }
     }
 
-    private void drawDecoration(Canvas c, SectionDecorationHandler decorationHandler, View parent) {
+    private void drawDecoration(Canvas c, SectionDecorationHandler decorationHandler,
+            RecyclerView parent) {
         if (decorationHandler == null) return;
         if (decorationHandler.mIsFullWidth) {
             decorationHandler.mBounds.left = parent.getPaddingLeft();
             decorationHandler.mBounds.right = parent.getWidth() - parent.getPaddingRight();
         }
         decorationHandler.onDraw(c);
+        if (mAppsView.getFloatingHeaderView().getFocusedChild() == null
+                && mAppsView.getApps().getFocusedChild() != null) {
+            int index = mAppsView.getApps().getFocusedChildIndex();
+            if (index >= 0 && index < parent.getChildCount()) {
+                decorationHandler.onFocusDraw(c, parent.getChildAt(index));
+            }
+        }
         decorationHandler.reset();
     }
 
@@ -95,18 +103,21 @@
         protected RectF mBounds = new RectF();
         private final boolean mIsFullWidth;
         private final float mRadius;
+
+        private final int mFocusColor;
         private final int mFillcolor;
-        Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 
 
         public SectionDecorationHandler(Context context, boolean isFullWidth) {
             mIsFullWidth = isFullWidth;
             mFillcolor = context.getColor(R.color.all_apps_section_fill);
+            mFocusColor = context.getColor(R.color.all_apps_section_focused_item);
             mRadius = Themes.getDialogCornerRadius(context);
         }
 
         /**
-         * Extends current bounds to include view
+         * Extends current bounds to include the view.
          */
         public void extendBounds(View view) {
             if (mBounds.isEmpty()) {
@@ -122,7 +133,7 @@
         }
 
         /**
-         * Draw bounds onto canvas
+         * Draw bounds onto canvas.
          */
         public void onDraw(Canvas canvas) {
             mPaint.setColor(mFillcolor);
@@ -130,7 +141,19 @@
         }
 
         /**
-         * Reset view bounds to empty
+         * Draw the bound of the view to the canvas.
+         */
+        public void onFocusDraw(Canvas canvas, @Nullable View view) {
+            if (view == null) {
+                return;
+            }
+            mPaint.setColor(mFocusColor);
+            canvas.drawRoundRect(view.getLeft(), view.getTop(),
+                    view.getRight(), view.getBottom(), mRadius, mRadius, mPaint);
+        }
+
+        /**
+         * Reset view bounds to empty.
          */
         public void reset() {
             mBounds.setEmpty();
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 9076f2a..7379dbed 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -224,6 +224,25 @@
     }
 
     /**
+     * Returns the child adapter item with IME launch focus.
+     */
+    public AdapterItem getFocusedChild() {
+        return mAdapterItems.get(getFocusedChildIndex());
+    }
+
+    /**
+     * Returns the index of the child with IME launch focus.
+     */
+    public int getFocusedChildIndex() {
+        for (AdapterItem item : mAdapterItems) {
+            if (item.isCountedForAccessibility()) {
+                return mAdapterItems.indexOf(item);
+            }
+        }
+        return -1;
+    }
+
+    /**
      * Returns the number of rows of applications
      */
     public int getNumAppRows() {
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderRow.java b/src/com/android/launcher3/allapps/FloatingHeaderRow.java
index f899587..e357f61 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderRow.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderRow.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.allapps;
 
 import android.graphics.Rect;
+import android.view.View;
 import android.view.animation.Interpolator;
 
 import com.android.launcher3.DeviceProfile;
@@ -55,4 +56,9 @@
     void setVerticalScroll(int scroll, boolean isScrolledOut);
 
     Class<? extends FloatingHeaderRow> getTypeClass();
+
+    /**
+     * Returns a child that has focus to be launched by the IME.
+     */
+    View getFocusedChild();
 }
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java
index 81e1b94..11d3fb9 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderView.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java
@@ -38,6 +38,7 @@
 import com.android.launcher3.Insettable;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.systemui.plugins.AllAppsRow;
 import com.android.systemui.plugins.AllAppsRow.OnHeightUpdatedListener;
@@ -194,6 +195,19 @@
         onHeightUpdated();
     }
 
+    @Override
+    public View getFocusedChild() {
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+            for (FloatingHeaderRow row : mAllRows) {
+                if (row.hasVisibleContent() && row.shouldDraw()) {
+                    return row.getFocusedChild();
+                }
+            }
+            return null;
+        }
+        return super.getFocusedChild();
+    }
+
     public void setup(AllAppsContainerView.AdapterHolder[] mAH, boolean tabsHidden) {
         for (FloatingHeaderRow row : mAllRows) {
             row.setup(this, mAllRows, tabsHidden);
diff --git a/src/com/android/launcher3/allapps/PluginHeaderRow.java b/src/com/android/launcher3/allapps/PluginHeaderRow.java
index 3089b18..cf7142c 100644
--- a/src/com/android/launcher3/allapps/PluginHeaderRow.java
+++ b/src/com/android/launcher3/allapps/PluginHeaderRow.java
@@ -83,4 +83,9 @@
     public Class<PluginHeaderRow> getTypeClass() {
         return PluginHeaderRow.class;
     }
+
+    @Override
+    public View getFocusedChild() {
+        return null;
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index db94e8b..06faaac 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -27,8 +27,11 @@
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.ExtendedEditText;
+import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AlphabeticalAppsList;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.PackageManagerHelper;
 
 import java.util.ArrayList;
@@ -101,6 +104,16 @@
 
     @Override
     public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+            if (actionId == EditorInfo.IME_ACTION_SEARCH) {
+                ItemInfo info = Launcher.getLauncher(mLauncher).getAppsView()
+                        .getHighlightedItemInfo();
+                if (info != null) {
+                    return mLauncher.startActivitySafely(v, info.getIntent(), info);
+                }
+            }
+        }
+
         // Skip if it's not the right action
         if (actionId != EditorInfo.IME_ACTION_SEARCH) {
             return false;
diff --git a/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java b/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java
index c50189c..ae7ad10 100644
--- a/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java
+++ b/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java
@@ -32,6 +32,8 @@
 import android.view.View.OnFocusChangeListener;
 
 import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.Themes;
 
 /**
  * A helper class to draw background of a focused view.
@@ -93,6 +95,7 @@
 
     private ObjectAnimator mCurrentAnimation;
     private float mAlpha;
+    private float mRadius;
 
     public FocusIndicatorHelper(View container) {
         mContainer = container;
@@ -104,6 +107,9 @@
 
         setAlpha(0);
         mShift = 0;
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+            mRadius = Themes.getDialogCornerRadius(container.getContext());
+        }
     }
 
     protected void setAlpha(float alpha) {
@@ -129,13 +135,15 @@
     }
 
     public void draw(Canvas c) {
-        if (mAlpha > 0) {
-            Rect newRect = getDrawRect();
-            if (newRect != null) {
-                mDirtyRect.set(newRect);
-                c.drawRect(mDirtyRect, mPaint);
-                mIsDirty = true;
-            }
+        if (mAlpha <= 0) return;
+
+        Rect newRect = getDrawRect();
+        if (newRect != null) {
+            mDirtyRect.set(newRect);
+            c.drawRoundRect((float) mDirtyRect.left, (float) mDirtyRect.top,
+                    (float) mDirtyRect.right, (float) mDirtyRect.bottom,
+                    mRadius, mRadius, mPaint);
+            mIsDirty = true;
         }
     }
 
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 0374009..2282339 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -313,7 +313,10 @@
         LAUNCHER_NAVIGATION_MODE_2_BUTTON(624),
 
         @UiEvent(doc = "System navigation mode is 0 button mode/gesture navigation mode .")
-        LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON(625);
+        LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON(625),
+
+        @UiEvent(doc = "User tapped on image content in Overview Select mode.")
+        LAUNCHER_SELECT_MODE_IMAGE(627);
 
         // ADD MORE
 
diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java
index 8b0ef7b..586333f 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -27,6 +27,7 @@
 import com.android.launcher3.LauncherModel.CallbackTask;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -76,18 +77,20 @@
         ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
         ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
         final IntArray orderedScreenIds = new IntArray();
+        ArrayList<FixedContainerItems> extraItems = new ArrayList<>();
 
         synchronized (mBgDataModel) {
             workspaceItems.addAll(mBgDataModel.workspaceItems);
             appWidgets.addAll(mBgDataModel.appWidgets);
             orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
+            mBgDataModel.extraItems.forEach(extraItems::add);
             mBgDataModel.lastBindId++;
             mMyBindingId = mBgDataModel.lastBindId;
         }
 
         for (Callbacks cb : mCallbacksList) {
             new WorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
-                    workspaceItems, appWidgets, orderedScreenIds).bind();
+                    workspaceItems, appWidgets, extraItems, orderedScreenIds).bind();
         }
     }
 
@@ -135,7 +138,7 @@
         private final ArrayList<ItemInfo> mWorkspaceItems;
         private final ArrayList<LauncherAppWidgetInfo> mAppWidgets;
         private final IntArray mOrderedScreenIds;
-
+        private final ArrayList<FixedContainerItems> mExtraItems;
 
         WorkspaceBinder(Callbacks callbacks,
                 Executor uiExecutor,
@@ -144,6 +147,7 @@
                 int myBindingId,
                 ArrayList<ItemInfo> workspaceItems,
                 ArrayList<LauncherAppWidgetInfo> appWidgets,
+                ArrayList<FixedContainerItems> extraItems,
                 IntArray orderedScreenIds) {
             mCallbacks = callbacks;
             mUiExecutor = uiExecutor;
@@ -152,6 +156,7 @@
             mMyBindingId = myBindingId;
             mWorkspaceItems = workspaceItems;
             mAppWidgets = appWidgets;
+            mExtraItems = extraItems;
             mOrderedScreenIds = orderedScreenIds;
         }
 
@@ -198,6 +203,8 @@
             // Load items on the current page.
             bindWorkspaceItems(currentWorkspaceItems, mainExecutor);
             bindAppWidgets(currentAppWidgets, mainExecutor);
+            mExtraItems.forEach(item ->
+                    executeCallbacksTask(c -> c.bindExtraContainerItems(item), mainExecutor));
 
             // Locate available spots for prediction using currentWorkspaceItems
             IntArray gaps = getMissingHotseatRanks(currentWorkspaceItems, idp.numHotseatIcons);
@@ -207,6 +214,7 @@
             // happens later).
             // This ensures that the first screen is immediately visible (eg. during rotation)
             // In case of !validFirstPage, bind all pages one after other.
+
             final Executor deferredExecutor =
                     validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;
 
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index fd8520d..140342f 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -424,6 +424,14 @@
         public FixedContainerItems clone() {
             return new FixedContainerItems(containerId, new ArrayList<>(items));
         }
+
+        public void setItems(List<ItemInfo> newItems) {
+            items.clear();
+            newItems.forEach(item -> {
+                item.container = containerId;
+                items.add(item);
+            });
+        }
     }
 
 
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 165d1ea..a27ac23 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -65,7 +65,7 @@
 
     private static final String TAG = "LoaderCursor";
 
-    public final LongSparseArray<UserHandle> allUsers;
+    private final LongSparseArray<UserHandle> allUsers;
 
     private final Uri mContentUri;
     private final Context mContext;
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index e89031e..1dd8c11 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -360,15 +360,12 @@
                 final int optionsIndex = c.getColumnIndexOrThrow(
                         LauncherSettings.Favorites.OPTIONS);
 
-                final LongSparseArray<UserHandle> allUsers = c.allUsers;
                 final LongSparseArray<Boolean> unlockedUsers = new LongSparseArray<>();
 
                 mUserManagerState.init(mUserCache, mUserManager);
 
                 for (UserHandle user : mUserCache.getUserProfiles()) {
                     long serialNo = mUserCache.getSerialNumberForUser(user);
-                    allUsers.put(serialNo, user);
-
                     boolean userUnlocked = mUserManager.isUserUnlocked(user);
 
                     // We can only query for shortcuts when the user is unlocked.
@@ -419,16 +416,6 @@
                             ComponentName cn = intent.getComponent();
                             targetPkg = cn == null ? intent.getPackage() : cn.getPackageName();
 
-                            if (allUsers.indexOfValue(c.user) < 0) {
-                                if (c.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
-                                    c.markDeleted("Legacy shortcuts are only allowed for current users");
-                                    continue;
-                                } else if (c.restoreFlag != 0) {
-                                    // Don't restore items for other profiles.
-                                    c.markDeleted("Restore from other profiles not supported");
-                                    continue;
-                                }
-                            }
                             if (TextUtils.isEmpty(targetPkg) &&
                                     c.itemType != LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
                                 c.markDeleted("Only legacy shortcuts can have null package");
@@ -773,7 +760,7 @@
             }
 
             // Load delegate items
-            mModelDelegate.loadItems();
+            mModelDelegate.loadItems(mUserManagerState, shortcutKeyToPinnedShortcuts);
 
             // Break early if we've stopped loading
             if (mStopped) {
diff --git a/src/com/android/launcher3/model/ModelDelegate.java b/src/com/android/launcher3/model/ModelDelegate.java
index ce4eed5..53e8a86 100644
--- a/src/com/android/launcher3/model/ModelDelegate.java
+++ b/src/com/android/launcher3/model/ModelDelegate.java
@@ -18,13 +18,17 @@
 import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
 
 import android.content.Context;
+import android.content.pm.ShortcutInfo;
 
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
+import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ResourceBasedOverride;
 
+import java.util.Map;
+
 /**
  * Class to extend LauncherModel functionality to provide extra data
  */
@@ -65,11 +69,12 @@
      * Load delegate items if any in the data model
      */
     @WorkerThread
-    public void loadItems() { }
+    public void loadItems(UserManagerState ums, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts) { }
 
     /**
      * Called when the delegate is no loner needed
      */
     @WorkerThread
     public void destroy() { }
+
 }
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index 59233cd..e03fd72 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -258,12 +258,6 @@
     }
 
     /**
-     * Can be overridden by inherited classes to fill in {@link LauncherAtom.ItemInfo}
-     */
-    public void setItemBuilder(LauncherAtom.ItemInfo.Builder builder) {
-    }
-
-    /**
      * Creates {@link LauncherAtom.ItemInfo} with important fields and parent container info.
      */
     public LauncherAtom.ItemInfo buildProto() {
diff --git a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
index b0d19a6..c04b7f0 100644
--- a/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
+++ b/src/com/android/launcher3/model/data/LauncherAppWidgetInfo.java
@@ -26,7 +26,6 @@
 import com.android.launcher3.AppWidgetResizeFrame;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.util.ContentWriter;
 
 /**
@@ -195,13 +194,4 @@
     public final boolean hasOptionFlag(int option) {
         return (options & option) != 0;
     }
-
-    @Override
-    public void setItemBuilder(LauncherAtom.ItemInfo.Builder builder) {
-        builder.setWidget(LauncherAtom.Widget.newBuilder()
-                .setSpanX(spanX)
-                .setSpanY(spanY)
-                .setComponentName(providerName.toString())
-                .setPackageName(providerName.getPackageName()));
-    }
 }
diff --git a/src/com/android/launcher3/states/StateAnimationConfig.java b/src/com/android/launcher3/states/StateAnimationConfig.java
index f90ad3c..8b72177 100644
--- a/src/com/android/launcher3/states/StateAnimationConfig.java
+++ b/src/com/android/launcher3/states/StateAnimationConfig.java
@@ -71,6 +71,7 @@
             ANIM_ALL_APPS_HEADER_FADE,
             ANIM_OVERVIEW_MODAL,
             ANIM_DEPTH,
+            ANIM_OVERVIEW_ACTIONS_FADE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AnimType {}
@@ -89,10 +90,11 @@
     public static final int ANIM_ALL_APPS_HEADER_FADE = 12; // e.g. predictions
     public static final int ANIM_OVERVIEW_MODAL = 13;
     public static final int ANIM_DEPTH = 14;
+    public static final int ANIM_OVERVIEW_ACTIONS_FADE = 15;
 
-    private static final int ANIM_TYPES_COUNT = 15;
+    private static final int ANIM_TYPES_COUNT = 16;
 
-    private final Interpolator[] mInterpolators = new Interpolator[ANIM_TYPES_COUNT];
+    protected final Interpolator[] mInterpolators = new Interpolator[ANIM_TYPES_COUNT];
 
     public StateAnimationConfig() { }
 
diff --git a/src/com/android/launcher3/util/IOUtils.java b/src/com/android/launcher3/util/IOUtils.java
index fcb96d7..1cec0ec 100644
--- a/src/com/android/launcher3/util/IOUtils.java
+++ b/src/com/android/launcher3/util/IOUtils.java
@@ -16,20 +16,19 @@
 
 package com.android.launcher3.util;
 
-import android.content.Context;
+import android.os.FileUtils;
 import android.util.Log;
 
+import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 
 import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.UUID;
 
 /**
  * Supports various IO utility functions
@@ -52,6 +51,9 @@
     }
 
     public static long copy(InputStream from, OutputStream to) throws IOException {
+        if (Utilities.ATLEAST_Q) {
+            return FileUtils.copy(from, to);
+        }
         byte[] buf = new byte[BUF_SIZE];
         long total = 0;
         int r;
@@ -62,25 +64,6 @@
         return total;
     }
 
-    /**
-     * Utility method to debug binary data
-     */
-    public static String createTempFile(Context context, byte[] data) {
-        if (!FeatureFlags.IS_STUDIO_BUILD) {
-            throw new IllegalStateException("Method only allowed in development mode");
-        }
-
-        String name = UUID.randomUUID().toString();
-        File file = new File(context.getCacheDir(), name);
-        try (FileOutputStream fo = new FileOutputStream(file)) {
-            fo.write(data);
-            fo.flush();
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-        return file.getAbsolutePath();
-    }
-
     public static void closeSilently(Closeable c) {
         if (c != null) {
             try {
diff --git a/src/com/android/launcher3/util/MultiValueAlpha.java b/src/com/android/launcher3/util/MultiValueAlpha.java
index a8642b0..5be9529 100644
--- a/src/com/android/launcher3/util/MultiValueAlpha.java
+++ b/src/com/android/launcher3/util/MultiValueAlpha.java
@@ -19,6 +19,8 @@
 import android.util.FloatProperty;
 import android.view.View;
 
+import com.android.launcher3.anim.AlphaUpdateListener;
+
 import java.util.Arrays;
 
 /**
@@ -44,6 +46,8 @@
     private final AlphaProperty[] mMyProperties;
 
     private int mValidMask;
+    // Whether we should change from INVISIBLE to VISIBLE and vice versa at low alpha values.
+    private boolean mUpdateVisibility;
 
     public MultiValueAlpha(View view, int size) {
         mView = view;
@@ -66,6 +70,11 @@
         return mMyProperties[index];
     }
 
+    /** Sets whether we should update between INVISIBLE and VISIBLE based on alpha. */
+    public void setUpdateVisibility(boolean updateVisibility) {
+        mUpdateVisibility = updateVisibility;
+    }
+
     public class AlphaProperty {
 
         private final int mMyMask;
@@ -99,6 +108,9 @@
             mValue = value;
 
             mView.setAlpha(mOthers * mValue);
+            if (mUpdateVisibility) {
+                AlphaUpdateListener.updateVisibility(mView);
+            }
         }
 
         public float getValue() {
diff --git a/src/com/android/launcher3/util/PersistedItemArray.java b/src/com/android/launcher3/util/PersistedItemArray.java
new file mode 100644
index 0000000..ae20638
--- /dev/null
+++ b/src/com/android/launcher3/util/PersistedItemArray.java
@@ -0,0 +1,180 @@
+/*
+ * 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.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.Xml;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.AutoInstallsLayout;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.pm.UserCache;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.LongFunction;
+
+/**
+ * Utility class to read/write a list of {@link com.android.launcher3.model.data.ItemInfo} on disk.
+ * This class is not thread safe, the caller should ensure proper threading
+ */
+public class PersistedItemArray<T extends ItemInfo> {
+
+    private static final String TAG = "PersistedItemArray";
+
+    private static final String TAG_ROOT = "items";
+    private static final String TAG_ENTRY = "entry";
+
+    private final String mFileName;
+
+    public PersistedItemArray(String fileName) {
+        mFileName = fileName + ".xml";
+    }
+
+    /**
+     * Writes the provided list of items on the disk
+     */
+    @WorkerThread
+    public void write(Context context, List<T> items) {
+        AtomicFile file = new AtomicFile(context.getFileStreamPath(mFileName));
+
+        FileOutputStream fos;
+        try {
+            fos = file.startWrite();
+        } catch (IOException e) {
+            Log.e(TAG, "Unable to persist items in " + mFileName, e);
+            return;
+        }
+
+        UserCache userCache = UserCache.INSTANCE.get(context);
+
+        try {
+            XmlSerializer out = Xml.newSerializer();
+            out.setOutput(fos, StandardCharsets.UTF_8.name());
+            out.startDocument(null, true);
+            out.startTag(null, TAG_ROOT);
+            for (T item : items) {
+                Intent intent = item.getIntent();
+                if (intent == null) {
+                    continue;
+                }
+
+                out.startTag(null, TAG_ENTRY);
+                out.attribute(null, Favorites.ITEM_TYPE, Integer.toString(item.itemType));
+                out.attribute(null, Favorites.PROFILE_ID,
+                        Long.toString(userCache.getSerialNumberForUser(item.user)));
+                out.attribute(null, Favorites.INTENT, intent.toUri(0));
+                out.endTag(null, TAG_ENTRY);
+            }
+            out.endTag(null, TAG_ROOT);
+            out.endDocument();
+        } catch (IOException e) {
+            file.failWrite(fos);
+            Log.e(TAG, "Unable to persist items in " + mFileName, e);
+            return;
+        }
+
+        file.finishWrite(fos);
+    }
+
+    /**
+     * Reads the items from the disk
+     */
+    @WorkerThread
+    public List<T> read(Context context, ItemFactory<T> factory) {
+        return read(context, factory, UserCache.INSTANCE.get(context)::getUserForSerialNumber);
+    }
+
+    /**
+     * Reads the items from the disk
+     * @param userFn method to provide user handle for a given user serial
+     */
+    @WorkerThread
+    public List<T> read(Context context, ItemFactory<T> factory, LongFunction<UserHandle> userFn) {
+        List<T> result = new ArrayList<>();
+        AtomicFile file = new AtomicFile(context.getFileStreamPath(mFileName));
+
+        try (FileInputStream fis = file.openRead()) {
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(new InputStreamReader(fis, StandardCharsets.UTF_8));
+
+            AutoInstallsLayout.beginDocument(parser, TAG_ROOT);
+            final int depth = parser.getDepth();
+
+            int type;
+            while (((type = parser.next()) != XmlPullParser.END_TAG
+                    || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+                if (type != XmlPullParser.START_TAG || !TAG_ENTRY.equals(parser.getName())) {
+                    continue;
+                }
+                try {
+                    int itemType = Integer.parseInt(
+                            parser.getAttributeValue(null, Favorites.ITEM_TYPE));
+                    UserHandle user = userFn.apply(Long.parseLong(
+                            parser.getAttributeValue(null, Favorites.PROFILE_ID)));
+                    Intent intent = Intent.parseUri(
+                            parser.getAttributeValue(null, Favorites.INTENT), 0);
+
+                    if (user != null && intent != null) {
+                        T item = factory.createInfo(itemType, user, intent);
+                        if (item != null) {
+                            result.add(item);
+                        }
+                    }
+                } catch (Exception e) {
+                    // Ignore this entry
+                }
+            }
+        } catch (FileNotFoundException e) {
+            // Ignore
+        } catch (IOException | XmlPullParserException e) {
+            Log.e(TAG, "Unable to read items in " + mFileName, e);
+            return Collections.emptyList();
+        }
+        return result;
+    }
+
+    /**
+     * Interface to create an ItemInfo during parsing
+     */
+    public interface ItemFactory<T extends ItemInfo> {
+
+        /**
+         * Returns an item info or null in which case the entry is ignored
+         */
+        @Nullable
+        T createInfo(int itemType, UserHandle user, Intent intent);
+    }
+}