Merge remote-tracking branch 'goog/ub-launcher3-master' into temp

Test: make builds

Change-Id: I4e845aa51c9247328159e5aa0d95d425bffa435c
diff --git a/Android.bp b/Android.bp
index e132854..4265a44 100644
--- a/Android.bp
+++ b/Android.bp
@@ -19,6 +19,7 @@
         "androidx.test.runner",
         "androidx.test.rules",
         "androidx.test.uiautomator_uiautomator",
+        "androidx.preference_preference",
         "SystemUISharedLib",
     ],
     srcs: [
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 8e01f82..4664c93 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -49,7 +49,7 @@
             android:stateNotNeeded="true"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
diff --git a/OWNERS b/OWNERS
index 3069afa..1d6ad8c 100644
--- a/OWNERS
+++ b/OWNERS
@@ -28,6 +28,7 @@
 peanutbutter@google.com
 xuqiu@google.com
 sreyasr@google.com
+thiruram@google.com
 
 per-file FeatureFlags.java, globs = set noparent
 per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, zakcohen@google.com, mrcasey@google.com, adamcohen@google.com, hyunyoungs@google.com
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
index 53910e3..19f85e4 100644
--- a/quickstep/AndroidManifest-launcher.xml
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -49,7 +49,7 @@
             android:stateNotNeeded="true"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 4e7c3fa..5be32a8 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -59,7 +59,7 @@
              android:stateNotNeeded="true"
              android:theme="@style/LauncherTheme"
              android:screenOrientation="unspecified"
-             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
              android:resizeableActivity="true"
              android:resumeWhilePausing="true"
              android:taskAffinity=""/>
diff --git a/quickstep/recents_ui_overrides/res/values/override.xml b/quickstep/recents_ui_overrides/res/values/override.xml
index 6aa9619..1937164 100644
--- a/quickstep/recents_ui_overrides/res/values/override.xml
+++ b/quickstep/recents_ui_overrides/res/values/override.xml
@@ -25,8 +25,11 @@
 
   <string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string>
 
-  <string name="user_event_dispatcher_class" translatable="false">com.android.quickstep.logging.UserEventDispatcherAppPredictionExtension</string>
+  <string name="user_event_dispatcher_class" translatable="false">com.android.quickstep.logging.UserEventDispatcherExtension</string>
 
   <string name="prediction_model_class" translatable="false">com.android.launcher3.hybridhotseat.HotseatPredictionModel</string>
+
+  <string name="model_delegate_class" translatable="false">com.android.launcher3.model.QuickstepModelDelegate</string>
+
 </resources>
 
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 e11c701..55384af 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
@@ -30,7 +30,6 @@
 import android.os.Build;
 import android.util.AttributeSet;
 import android.util.IntProperty;
-import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.animation.Interpolator;
@@ -44,19 +43,15 @@
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
-import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.allapps.FloatingHeaderRow;
 import com.android.launcher3.allapps.FloatingHeaderView;
 import com.android.launcher3.anim.AlphaUpdateListener;
 import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.keyboard.FocusIndicatorHelper;
 import com.android.launcher3.keyboard.FocusIndicatorHelper.SimpleFocusIndicatorHelper;
 import com.android.launcher3.logging.StatsLogUtils.LogContainerProvider;
-import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -67,15 +62,12 @@
 import com.android.quickstep.AnimatedFloat;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 @TargetApi(Build.VERSION_CODES.P)
 public class PredictionRowView extends LinearLayout implements
         LogContainerProvider, OnDeviceProfileChangeListener, FloatingHeaderRow {
 
-    private static final String TAG = "PredictionRowView";
-
     private static final IntProperty<PredictionRowView> TEXT_ALPHA =
             new IntProperty<PredictionRowView>("textAlpha") {
                 @Override
@@ -93,16 +85,14 @@
             (t) -> (t < 0.8f) ? 0 : (t - 0.8f) / 0.2f;
 
     private final Launcher mLauncher;
-    private final PredictionUiStateManager mPredictionUiStateManager;
     private int mNumPredictedAppsPerRow;
 
-    // The set of predicted app component names
-    private final List<ComponentKeyMapper> mPredictedAppComponents = new ArrayList<>();
-    // The set of predicted apps resolved from the component names and the current set of apps
-    private final ArrayList<ItemInfoWithIcon> mPredictedApps = new ArrayList<>();
     // Helper to drawing the focus indicator.
     private final FocusIndicatorHelper mFocusHelper;
 
+    // The set of predicted apps resolved from the component names and the current set of apps
+    private final List<WorkspaceItemInfo> mPredictedApps = new ArrayList<>();
+
     private final int mIconTextColor;
     private final int mIconFullTextAlpha;
     private int mIconLastSetTextAlpha;
@@ -134,8 +124,6 @@
         mLauncher = Launcher.getLauncher(context);
         mLauncher.addOnDeviceProfileChangeListener(this);
 
-        mPredictionUiStateManager = PredictionUiStateManager.INSTANCE.get(context);
-
         mIconTextColor = Themes.getAttrColor(context, android.R.attr.textColorSecondary);
         mIconFullTextAlpha = Color.alpha(mIconTextColor);
         mIconCurrentTextAlpha = mIconFullTextAlpha;
@@ -146,24 +134,9 @@
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-
-        mPredictionUiStateManager.setTargetAppsView(mLauncher.getAppsView());
-        getAppsStore().registerIconContainer(this);
         AllAppsTipView.scheduleShowIfNeeded(mLauncher);
     }
 
-    private AllAppsStore getAppsStore() {
-        return mLauncher.getAppsView().getAppsStore();
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-
-        mPredictionUiStateManager.setTargetAppsView(null);
-        getAppsStore().unregisterIconContainer(this);
-    }
-
     public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
         mParent = parent;
     }
@@ -205,7 +178,7 @@
      * Returns the predicted apps.
      */
     public List<ItemInfoWithIcon> getPredictedApps() {
-        return mPredictedApps;
+        return new ArrayList<>(mPredictedApps);
     }
 
     /**
@@ -217,12 +190,12 @@
      * If the number of predicted apps is the same as the previous list of predicted apps,
      * we can optimize by swapping them in place.
      */
-    public void setPredictedApps(List<ComponentKeyMapper> apps) {
-        mPredictedAppComponents.clear();
-        mPredictedAppComponents.addAll(apps);
-
+    public void setPredictedApps(List<ItemInfo> items) {
         mPredictedApps.clear();
-        mPredictedApps.addAll(processPredictedAppComponents(mPredictedAppComponents));
+        items.stream()
+                .filter(itemInfo -> itemInfo instanceof WorkspaceItemInfo)
+                .map(itemInfo -> (WorkspaceItemInfo) itemInfo)
+                .forEach(mPredictedApps::add);
         applyPredictionApps();
     }
 
@@ -264,11 +237,7 @@
             icon.reset();
             if (predictionCount > i) {
                 icon.setVisibility(View.VISIBLE);
-                if (mPredictedApps.get(i) instanceof AppInfo) {
-                    icon.applyFromApplicationInfo((AppInfo) mPredictedApps.get(i));
-                } else if (mPredictedApps.get(i) instanceof WorkspaceItemInfo) {
-                    icon.applyFromWorkspaceItem((WorkspaceItemInfo) mPredictedApps.get(i));
-                }
+                icon.applyFromWorkspaceItem(mPredictedApps.get(i));
                 icon.setTextColor(iconColor);
             } else {
                 icon.setVisibility(predictionCount == 0 ? GONE : INVISIBLE);
@@ -284,33 +253,6 @@
         mParent.onHeightUpdated();
     }
 
-    private List<ItemInfoWithIcon> processPredictedAppComponents(
-            List<ComponentKeyMapper> components) {
-        if (getAppsStore().getApps().length == 0) {
-            // Apps have not been bound yet.
-            return Collections.emptyList();
-        }
-
-        List<ItemInfoWithIcon> predictedApps = new ArrayList<>();
-        for (ComponentKeyMapper mapper : components) {
-            ItemInfoWithIcon info = mapper.getApp(getAppsStore());
-            if (info != null) {
-                ItemInfoWithIcon predictedApp = info.clone();
-                predictedApp.container = LauncherSettings.Favorites.CONTAINER_PREDICTION;
-                predictedApps.add(predictedApp);
-            } else {
-                if (FeatureFlags.IS_STUDIO_BUILD) {
-                    Log.e(TAG, "Predicted app not found: " + mapper);
-                }
-            }
-            // Stop at the number of predicted apps
-            if (predictedApps.size() == mNumPredictedAppsPerRow) {
-                break;
-            }
-        }
-        return predictedApps;
-    }
-
     @Override
     public void fillInLogContainerData(ItemInfo childInfo, LauncherLogProto.Target child,
             ArrayList<LauncherLogProto.Target> parents) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
deleted file mode 100644
index 830c103..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
+++ /dev/null
@@ -1,348 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.appprediction;
-
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
-import static com.android.launcher3.LauncherState.BACKGROUND_APP;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-
-import android.app.prediction.AppPredictor;
-import android.app.prediction.AppTarget;
-import android.content.ComponentName;
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.allapps.AllAppsContainerView;
-import com.android.launcher3.allapps.AllAppsStore.OnUpdateListener;
-import com.android.launcher3.hybridhotseat.HotseatPredictionController;
-import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.ItemInfoWithIcon;
-import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.statemanager.StateManager.StateListener;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.MainThreadInitializedObject;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.OptionalInt;
-import java.util.stream.IntStream;
-
-/**
- * Handler responsible to updating the UI due to predicted apps changes. Operations:
- * 1) Pushes the predicted apps to all-apps. If all-apps is visible, waits until it becomes
- * invisible again before applying the changes. This ensures that the UI does not change abruptly
- * in front of the user, even if an app launched and user pressed back button to return to the
- * all-apps UI again.
- * 2) Prefetch high-res icons for predicted apps. This ensures that we have the icons in memory
- * even if all-apps is not opened as they are shown in search UI as well
- * 3) Load instant app if it is not already in memory. As predictions are persisted on disk,
- * instant app will not be in memory when launcher starts.
- * 4) Maintains the current active client id (for the predictions) and all updates are performed on
- * that client id.
- */
-public class PredictionUiStateManager implements StateListener<LauncherState>,
-        ItemInfoUpdateReceiver, OnIDPChangeListener, OnUpdateListener {
-
-    public static final String LAST_PREDICTION_ENABLED_STATE = "last_prediction_enabled_state";
-
-    // TODO (b/129421797): Update the client constants
-    public enum Client {
-        HOME("home");
-
-        public final String id;
-
-        Client(String id) {
-            this.id = id;
-        }
-    }
-
-    public static final MainThreadInitializedObject<PredictionUiStateManager> INSTANCE =
-            new MainThreadInitializedObject<>(PredictionUiStateManager::new);
-
-    private final Context mContext;
-
-    private final DynamicItemCache mDynamicItemCache;
-    private List mPredictionServicePredictions = Collections.emptyList();
-
-    private int mMaxIconsPerRow;
-
-    private AllAppsContainerView mAppsView;
-
-    private PredictionState mPendingState;
-    private PredictionState mCurrentState;
-
-    private boolean mGettingValidPredictionResults;
-
-    private PredictionUiStateManager(Context context) {
-        mContext = context;
-
-        mDynamicItemCache = new DynamicItemCache(context, this::onAppsUpdated);
-
-        InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
-        mMaxIconsPerRow = idp.numColumns;
-
-        idp.addOnChangeListener(this);
-        mGettingValidPredictionResults = Utilities.getDevicePrefs(context)
-                .getBoolean(LAST_PREDICTION_ENABLED_STATE, true);
-
-        // Call this last
-        mCurrentState = parseLastState();
-    }
-
-    @Override
-    public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
-        mMaxIconsPerRow = profile.numColumns;
-    }
-
-    public void setTargetAppsView(AllAppsContainerView appsView) {
-        if (mAppsView != null) {
-            mAppsView.getAppsStore().removeUpdateListener(this);
-        }
-        mAppsView = appsView;
-        if (mAppsView != null) {
-            mAppsView.getAppsStore().addUpdateListener(this);
-        }
-        if (mPendingState != null) {
-            applyState(mPendingState);
-            mPendingState = null;
-        } else {
-            applyState(mCurrentState);
-        }
-        updateDependencies(mCurrentState);
-    }
-
-    @Override
-    public void reapplyItemInfo(ItemInfoWithIcon info) { }
-
-    @Override
-    public void onStateTransitionComplete(LauncherState state) {
-        if (mAppsView == null) {
-            return;
-        }
-        if (mPendingState != null && canApplyPredictions(mPendingState)) {
-            applyState(mPendingState);
-            mPendingState = null;
-        }
-        if (mPendingState == null) {
-            Launcher.getLauncher(mAppsView.getContext()).getStateManager()
-                    .removeStateListener(this);
-        }
-    }
-
-    private void scheduleApplyPredictedApps(PredictionState state) {
-        boolean registerListener = mPendingState == null;
-        mPendingState = state;
-        if (registerListener) {
-            // Add a listener and wait until appsView is invisible again.
-            Launcher.getLauncher(mAppsView.getContext()).getStateManager().addStateListener(this);
-        }
-    }
-
-    private void applyState(PredictionState state) {
-        mCurrentState = state;
-        if (mAppsView != null) {
-            mAppsView.getFloatingHeaderView().findFixedRowByType(PredictionRowView.class)
-                    .setPredictedApps(mCurrentState.apps);
-        }
-    }
-
-    private void updatePredictionStateAfterCallback() {
-        boolean validResults = mPredictionServicePredictions != null
-                && !mPredictionServicePredictions.isEmpty();
-        if (validResults != mGettingValidPredictionResults) {
-            mGettingValidPredictionResults = validResults;
-            Utilities.getDevicePrefs(mContext).edit()
-                    .putBoolean(LAST_PREDICTION_ENABLED_STATE, true)
-                    .apply();
-        }
-        dispatchOnChange(true);
-    }
-
-    public AppPredictor.Callback appPredictorCallback(Client client) {
-        return targets -> {
-            mPredictionServicePredictions = targets;
-            updatePredictionStateAfterCallback();
-        };
-    }
-
-    private void dispatchOnChange(boolean changed) {
-        PredictionState newState = changed
-                ? parseLastState()
-                : mPendingState != null && canApplyPredictions(mPendingState)
-                        ? mPendingState
-                        : mCurrentState;
-        if (changed && mAppsView != null && !canApplyPredictions(newState)) {
-            scheduleApplyPredictedApps(newState);
-        } else {
-            applyState(newState);
-        }
-    }
-
-    private PredictionState parseLastState() {
-        PredictionState state = new PredictionState();
-        state.isEnabled = mGettingValidPredictionResults;
-        if (!state.isEnabled) {
-            state.apps = Collections.EMPTY_LIST;
-            return state;
-        }
-
-        state.apps = new ArrayList<>();
-
-        List<AppTarget> appTargets = mPredictionServicePredictions;
-        if (!appTargets.isEmpty()) {
-            for (AppTarget appTarget : appTargets) {
-                ComponentKey key;
-                if (appTarget.getShortcutInfo() != null) {
-                    key = ShortcutKey.fromInfo(appTarget.getShortcutInfo());
-                } else {
-                    key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
-                            appTarget.getClassName()), appTarget.getUser());
-                }
-                state.apps.add(new ComponentKeyMapper(key, mDynamicItemCache));
-            }
-        }
-        updateDependencies(state);
-        return state;
-    }
-
-    private void updateDependencies(PredictionState state) {
-        if (!state.isEnabled || mAppsView == null) {
-            return;
-        }
-        mDynamicItemCache.updateDependencies(state.apps, mAppsView.getAppsStore(), this,
-                mMaxIconsPerRow);
-    }
-
-    @Override
-    public void onAppsUpdated() {
-        dispatchOnChange(false);
-    }
-
-    private boolean canApplyPredictions(PredictionState newState) {
-        if (mAppsView == null) {
-            // If there is no apps view, no need to schedule.
-            return true;
-        }
-        Launcher launcher = Launcher.getLauncher(mAppsView.getContext());
-        PredictionRowView predictionRow = mAppsView.getFloatingHeaderView().
-                findFixedRowByType(PredictionRowView.class);
-        if (!predictionRow.isShown() || predictionRow.getAlpha() == 0 ||
-                launcher.isForceInvisible()) {
-            return true;
-        }
-
-        if (mCurrentState.isEnabled != newState.isEnabled
-                || mCurrentState.apps.isEmpty() != newState.apps.isEmpty()) {
-            // If the visibility of the prediction row is changing, apply immediately.
-            return true;
-        }
-
-        if (launcher.getDeviceProfile().isVerticalBarLayout()) {
-            // If we are here & mAppsView.isShown() = true, we are probably in all-apps or mid way
-            return false;
-        }
-        if (!launcher.isInState(OVERVIEW) && !launcher.isInState(BACKGROUND_APP)) {
-            // Just a fallback as we dont need to apply instantly, if we are not in the swipe-up UI
-            return false;
-        }
-
-        // Instead of checking against 1, we should check against (1 + delta), where delta accounts
-        // for the nav-bar height (as app icon can still be visible under the nav-bar). Checking
-        // against 1, keeps the logic simple :)
-        return launcher.getAllAppsController().getProgress() > 1;
-    }
-
-    public PredictionState getCurrentState() {
-        return mCurrentState;
-    }
-
-    /**
-     * Returns ranking info for the app within all apps prediction.
-     * Only applicable when {@link ItemInfo#itemType} is one of the followings:
-     * {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION},
-     * {@link LauncherSettings.Favorites#ITEM_TYPE_SHORTCUT},
-     * {@link LauncherSettings.Favorites#ITEM_TYPE_DEEP_SHORTCUT}
-     */
-    public OptionalInt getAllAppsRank(@Nullable ItemInfo itemInfo) {
-        if (itemInfo == null || itemInfo.getTargetComponent() == null || itemInfo.user == null) {
-            return OptionalInt.empty();
-        }
-
-        if (itemInfo.itemType == ITEM_TYPE_APPLICATION
-                || itemInfo.itemType == ITEM_TYPE_SHORTCUT
-                || itemInfo.itemType == ITEM_TYPE_DEEP_SHORTCUT) {
-            ComponentKey key = new ComponentKey(itemInfo.getTargetComponent(),
-                    itemInfo.user);
-            final List<ComponentKeyMapper> apps = getCurrentState().apps;
-            return IntStream.range(0, apps.size())
-                    .filter(index -> key.equals(apps.get(index).getComponentKey()))
-                    .findFirst();
-        }
-
-        return OptionalInt.empty();
-    }
-
-    /**
-     * Fill in predicted_rank field based on app prediction.
-     * Only applicable when {@link ItemInfo#itemType} is one of the followings:
-     * {@link LauncherSettings.Favorites#ITEM_TYPE_APPLICATION},
-     * {@link LauncherSettings.Favorites#ITEM_TYPE_SHORTCUT},
-     * {@link LauncherSettings.Favorites#ITEM_TYPE_DEEP_SHORTCUT}
-     */
-    public static void fillInPredictedRank(
-            @NonNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target) {
-
-        final PredictionUiStateManager manager = PredictionUiStateManager.INSTANCE.getNoCreate();
-        if (manager == null || itemInfo.getTargetComponent() == null || itemInfo.user == null
-                || (itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
-                && itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT
-                && itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)) {
-            return;
-        }
-        if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_PREDICTION) {
-            HotseatPredictionController.encodeHotseatLayoutIntoPredictionRank(itemInfo, target);
-            return;
-        }
-
-        final ComponentKey k = new ComponentKey(itemInfo.getTargetComponent(), itemInfo.user);
-        final List<ComponentKeyMapper> predictedApps = manager.getCurrentState().apps;
-        IntStream.range(0, predictedApps.size())
-                .filter((i) -> k.equals(predictedApps.get(i).getComponentKey()))
-                .findFirst()
-                .ifPresent((rank) -> target.predictedRank = 0 - rank);
-    }
-
-    public static class PredictionState {
-
-        public boolean isEnabled;
-        public List<ComponentKeyMapper> apps;
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java
index c15a596..20e1edc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java
@@ -71,10 +71,9 @@
     }
 
     private PrintWriter getWriter() {
-        String fName = FILE_NAME_PREFIX + (LOG_DAYS % 10);
-        if (fName.equals(mFileName)) return mCurrentWriter;
-
         Calendar cal = Calendar.getInstance();
+        String fName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) % 10);
+        if (fName.equals(mFileName)) return mCurrentWriter;
 
         boolean append = false;
         File logFile = new File(mLogsDir, fName);
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
index 8c1db4e..9e7c9fb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatRestoreHelper.java
@@ -30,7 +30,6 @@
  */
 public class HotseatRestoreHelper {
     private final Launcher mLauncher;
-    private boolean mBackupRestored = false;
 
     HotseatRestoreHelper(Launcher context) {
         mLauncher = context;
@@ -62,7 +61,6 @@
      * Finds and restores a previously saved snapshow of Favorites table
      */
     public void restoreBackup() {
-        if (mBackupRestored) return;
         MODEL_EXECUTOR.execute(() -> {
             try (LauncherDbUtils.SQLiteTransaction transaction = (LauncherDbUtils.SQLiteTransaction)
                     LauncherSettings.Settings.call(
@@ -78,7 +76,6 @@
                         idp.numRows);
                 backupTable.restoreFromCustomBackupTable(HYBRID_HOTSEAT_BACKUP_TABLE, true);
                 transaction.commit();
-                mBackupRestored = true;
                 mLauncher.getModel().forceReload();
             }
         });
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/AppEventProducer.java
similarity index 64%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
rename to quickstep/recents_ui_overrides/src/com/android/launcher3/model/AppEventProducer.java
index 8cabe3d..8e4c43f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/AppEventProducer.java
@@ -13,175 +13,76 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.launcher3.appprediction;
+package com.android.launcher3.model;
 
-import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DROPPED_ON_DONT_SUGGEST;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.annotation.TargetApi;
-import android.app.prediction.AppPredictionContext;
-import android.app.prediction.AppPredictionManager;
-import android.app.prediction.AppPredictor;
 import android.app.prediction.AppTarget;
 import android.app.prediction.AppTargetEvent;
 import android.app.prediction.AppTargetId;
 import android.content.ComponentName;
 import android.content.Context;
 import android.os.Build;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
 import android.os.Process;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.text.TextUtils;
-import android.util.Log;
 
 import androidx.annotation.AnyThread;
 import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
 import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
 import com.android.launcher3.logger.LauncherAtom.FolderContainer;
 import com.android.launcher3.logger.LauncherAtom.HotseatContainer;
 import com.android.launcher3.logger.LauncherAtom.WorkspaceContainer;
 import com.android.launcher3.logging.StatsLogManager.EventEnum;
-import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.pm.UserCache;
-import com.android.quickstep.logging.StatsLogCompatManager;
 import com.android.quickstep.logging.StatsLogCompatManager.StatsLogConsumer;
 
 import java.util.Locale;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 /**
- * Subclass of app tracker which publishes the data to the prediction engine and gets back results.
+ * Utility class to track stats log and emit corresponding app events
  */
-@TargetApi(Build.VERSION_CODES.Q)
-public class PredictionAppTracker extends AppLaunchTracker implements StatsLogConsumer {
+@TargetApi(Build.VERSION_CODES.R)
+public class AppEventProducer implements StatsLogConsumer {
 
-    private static final String TAG = "PredictionAppTracker";
-    private static final boolean DBG = false;
+    private static final int MSG_LAUNCH = 0;
 
-    private static final int MSG_INIT = 0;
-    private static final int MSG_DESTROY = 1;
-    private static final int MSG_LAUNCH = 2;
-    private static final int MSG_PREDICT = 3;
-
-    protected final Context mContext;
+    private final Context mContext;
     private final Handler mMessageHandler;
+    private final Consumer<AppTargetEvent> mCallback;
 
-    // Accessed only on worker thread
-    private AppPredictor mHomeAppPredictor;
-
-    public PredictionAppTracker(Context context) {
+    public AppEventProducer(Context context, Consumer<AppTargetEvent> callback) {
         mContext = context;
-        mMessageHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessage);
-        InvariantDeviceProfile.INSTANCE.get(mContext).addOnChangeListener(this::onIdpChanged);
-
-        mMessageHandler.sendEmptyMessage(MSG_INIT);
-    }
-
-    @UiThread
-    private void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
-        if ((changeFlags & CHANGE_FLAG_GRID) != 0) {
-            // Reinitialize everything
-            mMessageHandler.sendEmptyMessage(MSG_INIT);
-        }
-    }
-
-    @WorkerThread
-    private void destroy() {
-        if (mHomeAppPredictor != null) {
-            mHomeAppPredictor.destroy();
-            mHomeAppPredictor = null;
-        }
-        StatsLogCompatManager.LOGS_CONSUMER.remove(this);
-    }
-
-    @WorkerThread
-    private AppPredictor createPredictor(Client client, int count) {
-        AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class);
-
-        if (apm == null) {
-            return null;
-        }
-
-        AppPredictor predictor = apm.createAppPredictionSession(
-                new AppPredictionContext.Builder(mContext)
-                        .setUiSurface(client.id)
-                        .setPredictedTargetCount(count)
-                        .setExtras(getAppPredictionContextExtras(client))
-                        .build());
-        predictor.registerPredictionUpdates(mContext.getMainExecutor(),
-                PredictionUiStateManager.INSTANCE.get(mContext).appPredictorCallback(client));
-        predictor.requestPredictionUpdate();
-        return predictor;
-    }
-
-    /**
-     * Override to add custom extras.
-     */
-    @WorkerThread
-    @Nullable
-    public Bundle getAppPredictionContextExtras(Client client) {
-        return null;
+        mMessageHandler = new Handler(MODEL_EXECUTOR.getLooper(), this::handleMessage);
+        mCallback = callback;
     }
 
     @WorkerThread
     private boolean handleMessage(Message msg) {
         switch (msg.what) {
-            case MSG_INIT: {
-                // Destroy any existing clients
-                destroy();
-
-                // Initialize the clients
-                int count = InvariantDeviceProfile.INSTANCE.get(mContext).numAllAppsColumns;
-                mHomeAppPredictor = createPredictor(Client.HOME, count);
-                StatsLogCompatManager.LOGS_CONSUMER.add(this);
-                return true;
-            }
-            case MSG_DESTROY: {
-                destroy();
-                return true;
-            }
             case MSG_LAUNCH: {
-                if (mHomeAppPredictor != null) {
-                    mHomeAppPredictor.notifyAppTargetEvent((AppTargetEvent) msg.obj);
-                }
-                return true;
-            }
-            case MSG_PREDICT: {
-                if (mHomeAppPredictor != null) {
-                    mHomeAppPredictor.requestPredictionUpdate();
-                }
+                mCallback.accept((AppTargetEvent) msg.obj);
                 return true;
             }
         }
         return false;
     }
 
-    @Override
-    @UiThread
-    public void onReturnedToHome() {
-        String client = Client.HOME.id;
-        mMessageHandler.removeMessages(MSG_PREDICT, client);
-        Message.obtain(mMessageHandler, MSG_PREDICT, client).sendToTarget();
-        if (DBG) {
-            Log.d(TAG, String.format("Sent immediate message to update %s", client));
-        }
-    }
-
     @AnyThread
     private void sendEvent(LauncherAtom.ItemInfo atomInfo, int eventId) {
         AppTarget target = toAppTarget(atomInfo);
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
new file mode 100644
index 0000000..721e2be
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/PredictionUpdateTask.java
@@ -0,0 +1,117 @@
+/*
+ * 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.model;
+
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+import static com.android.launcher3.model.QuickstepModelDelegate.LAST_PREDICTION_ENABLED_STATE;
+import static com.android.quickstep.InstantAppResolverImpl.COMPONENT_CLASS_MARKER;
+
+import android.app.prediction.AppTarget;
+import android.content.ComponentName;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.ShortcutInfo;
+import android.os.UserHandle;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Task to update model as a result of predicted apps update
+ */
+public class PredictionUpdateTask extends BaseModelUpdateTask {
+
+    private final List<AppTarget> mTargets;
+    private final int mContainerId;
+
+    PredictionUpdateTask(int containerId, List<AppTarget> targets) {
+        mContainerId = containerId;
+        mTargets = targets;
+    }
+
+    @Override
+    public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+        // TODO: persist the whole list
+        Utilities.getDevicePrefs(app.getContext()).edit()
+                .putBoolean(LAST_PREDICTION_ENABLED_STATE, !mTargets.isEmpty()).apply();
+
+        FixedContainerItems fci;
+        synchronized (dataModel) {
+            fci = dataModel.extraItems.get(mContainerId);
+            if (fci == null) {
+                return;
+            }
+        }
+
+        Set<UserHandle> usersForChangedShortcuts = new HashSet<>(fci.items.stream()
+                .filter(info -> info.itemType == ITEM_TYPE_DEEP_SHORTCUT)
+                .map(info -> info.user)
+                .collect(Collectors.toSet()));
+        fci.items.clear();
+
+        for (AppTarget target : mTargets) {
+            WorkspaceItemInfo itemInfo;
+            ShortcutInfo si = target.getShortcutInfo();
+            if (si != null) {
+                usersForChangedShortcuts.add(si.getUserHandle());
+                itemInfo = new WorkspaceItemInfo(si, app.getContext());
+                app.getIconCache().getShortcutIcon(itemInfo, si);
+            } else {
+                String className = target.getClassName();
+                if (COMPONENT_CLASS_MARKER.equals(className)) {
+                    // TODO: Implement this
+                    continue;
+                }
+                ComponentName cn = new ComponentName(target.getPackageName(), className);
+                UserHandle user = target.getUser();
+                itemInfo = apps.data.stream()
+                        .filter(info -> user.equals(info.user) && cn.equals(info.componentName))
+                        .map(AppInfo::makeWorkspaceItem)
+                        .findAny()
+                        .orElseGet(() -> {
+                            LauncherActivityInfo lai = app.getContext()
+                                    .getSystemService(LauncherApps.class)
+                                    .resolveActivity(AppInfo.makeLaunchIntent(cn), user);
+                            if (lai == null) {
+                                return null;
+                            }
+                            AppInfo ai = new AppInfo(app.getContext(), lai, user);
+                            app.getIconCache().getTitleAndIcon(ai, lai, false);
+                            return ai.makeWorkspaceItem();
+                        });
+
+                if (itemInfo == null) {
+                    continue;
+                }
+            }
+
+            itemInfo.container = mContainerId;
+            fci.items.add(itemInfo);
+        }
+
+        bindExtraContainerItems(fci);
+        usersForChangedShortcuts.forEach(
+                u -> dataModel.updateShortcutPinnedState(app.getContext(), u));
+    }
+}
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
new file mode 100644
index 0000000..b516469
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -0,0 +1,138 @@
+/*
+ * 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.model;
+
+import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
+
+import android.app.prediction.AppPredictionContext;
+import android.app.prediction.AppPredictionManager;
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.content.Context;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.util.Executors;
+import com.android.quickstep.logging.StatsLogCompatManager;
+
+import java.util.List;
+
+/**
+ * Model delegate which loads prediction items
+ */
+public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChangeListener {
+
+    public static final String LAST_PREDICTION_ENABLED_STATE = "last_prediction_enabled_state";
+
+    private final InvariantDeviceProfile mIDP;
+    private final AppEventProducer mAppEventProducer;
+
+    private AppPredictor mAllAppsPredictor;
+    private boolean mActive = false;
+
+    public QuickstepModelDelegate(Context context) {
+        mAppEventProducer = new AppEventProducer(context, this::onAppTargetEvent);
+
+        mIDP = InvariantDeviceProfile.INSTANCE.get(context);
+        mIDP.addOnChangeListener(this);
+        StatsLogCompatManager.LOGS_CONSUMER.add(mAppEventProducer);
+    }
+
+    @Override
+    public void loadItems() {
+        // TODO: Implement caching and preloading
+        super.loadItems();
+        mDataModel.extraItems.put(
+                CONTAINER_PREDICTION, new FixedContainerItems(CONTAINER_PREDICTION));
+
+        mActive = true;
+        recreatePredictors();
+    }
+
+    @Override
+    public void validateData() {
+        super.validateData();
+        if (mAllAppsPredictor != null) {
+            mAllAppsPredictor.requestPredictionUpdate();
+        }
+    }
+
+    @Override
+    public void destroy() {
+        super.destroy();
+        mActive = false;
+        StatsLogCompatManager.LOGS_CONSUMER.remove(mAppEventProducer);
+
+        destroyPredictors();
+        mIDP.removeOnChangeListener(this);
+    }
+
+    private void destroyPredictors() {
+        if (mAllAppsPredictor != null) {
+            mAllAppsPredictor.destroy();
+            mAllAppsPredictor = null;
+        }
+    }
+
+    @WorkerThread
+    private void recreatePredictors() {
+        destroyPredictors();
+        if (!mActive) {
+            return;
+        }
+
+        Context context = mApp.getContext();
+        AppPredictionManager apm = context.getSystemService(AppPredictionManager.class);
+        if (apm == null) {
+            return;
+        }
+
+        int count = mIDP.numAllAppsColumns;
+
+        mAllAppsPredictor = apm.createAppPredictionSession(
+                new AppPredictionContext.Builder(context)
+                        .setUiSurface("home")
+                        .setPredictedTargetCount(count)
+                        .build());
+        mAllAppsPredictor.registerPredictionUpdates(
+                Executors.MODEL_EXECUTOR, this::onAllAppsPredictionChanged);
+        mAllAppsPredictor.requestPredictionUpdate();
+    }
+
+    private void onAllAppsPredictionChanged(List<AppTarget> targets) {
+        mApp.getModel().enqueueModelUpdateTask(
+                new PredictionUpdateTask(CONTAINER_PREDICTION, targets));
+    }
+
+    @Override
+    public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
+        if ((changeFlags & CHANGE_FLAG_GRID) != 0) {
+            // Reinitialize everything
+            Executors.MODEL_EXECUTOR.execute(this::recreatePredictors);
+        }
+    }
+
+    private void onAppTargetEvent(AppTargetEvent event) {
+        if (mAllAppsPredictor != null) {
+            mAllAppsPredictor.notifyAppTargetEvent(event);
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index bdbd1e9..cf86cd5 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -17,6 +17,9 @@
 
 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
 
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
@@ -37,16 +40,18 @@
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.appprediction.PredictionUiStateManager;
+import com.android.launcher3.appprediction.PredictionRowView;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.hybridhotseat.HotseatPredictionController;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
+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;
@@ -79,7 +84,7 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.OptionalInt;
+import java.util.Objects;
 import java.util.stream.Stream;
 
 public class QuickstepLauncher extends BaseQuickstepLauncher {
@@ -91,6 +96,8 @@
     public static final AsyncCommand SET_SHELF_HEIGHT = (context, arg1, arg2) ->
             SystemUiProxy.INSTANCE.get(context).setShelfHeight(arg1 != 0, arg2);
 
+    private FixedContainerItems mAllAppsPredictions;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -111,8 +118,23 @@
     protected void logAppLaunch(ItemInfo info, InstanceId instanceId) {
         StatsLogger logger = getStatsLogManager()
                 .logger().withItemInfo(info).withInstanceId(instanceId);
-        OptionalInt allAppsRank = PredictionUiStateManager.INSTANCE.get(this).getAllAppsRank(info);
-        allAppsRank.ifPresent(logger::withRank);
+
+        if (mAllAppsPredictions != null
+                && (info.itemType == ITEM_TYPE_APPLICATION
+                || info.itemType == ITEM_TYPE_SHORTCUT
+                || info.itemType == ITEM_TYPE_DEEP_SHORTCUT)) {
+            int count = mAllAppsPredictions.items.size();
+            for (int i = 0; i < count; i++) {
+                ItemInfo targetInfo = mAllAppsPredictions.items.get(i);
+                if (targetInfo.itemType == info.itemType
+                        && targetInfo.user.equals(info.user)
+                        && Objects.equals(targetInfo.getIntent(), info.getIntent())) {
+                    logger.withRank(i);
+                    break;
+                }
+
+            }
+        }
         logger.log(LAUNCHER_APP_LAUNCH_TAP);
 
         if (mHotseatPredictionController != null) {
@@ -201,6 +223,15 @@
     }
 
     @Override
+    public void bindExtraContainerItems(FixedContainerItems item) {
+        if (item.containerId == Favorites.CONTAINER_PREDICTION) {
+            mAllAppsPredictions = item;
+            getAppsView().getFloatingHeaderView().findFixedRowByType(PredictionRowView.class)
+                    .setPredictedApps(item.items);
+        }
+    }
+
+    @Override
     public void onDestroy() {
         super.onDestroy();
         if (mHotseatPredictionController != null) {
@@ -216,10 +247,8 @@
         switch (state.ordinal) {
             case HINT_STATE_ORDINAL: {
                 Workspace workspace = getWorkspace();
-                boolean willMoveScreens = workspace.getNextPage() != Workspace.DEFAULT_PAGE;
-                getStateManager().goToState(NORMAL, true,
-                        willMoveScreens ? null : getScrimView()::startDragHandleEducationAnim);
-                if (willMoveScreens) {
+                getStateManager().goToState(NORMAL);
+                if (workspace.getNextPage() != Workspace.DEFAULT_PAGE) {
                     workspace.post(workspace::moveToDefaultScreen);
                 }
                 break;
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 a0af797..131fcbf 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
@@ -17,7 +17,6 @@
 
 import static android.view.View.VISIBLE;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.HINT_STATE;
 import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
@@ -52,6 +51,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
@@ -212,7 +212,7 @@
                 // Scale up the recents, if it is not coming from the side
                 RecentsView overview = mActivity.getOverviewPanel();
                 if (overview.getVisibility() != VISIBLE || overview.getContentAlpha() == 0) {
-                    SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
+                    RECENTS_SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
                 }
             }
             config.setInterpolator(ANIM_WORKSPACE_FADE, OVERSHOOT_1_2);
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 c1a585e..e45fa9d 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
@@ -159,8 +159,7 @@
             builder.setFloat(recentsView, ADJACENT_PAGE_OFFSET,
                     -mPullbackDistance / recentsView.getPageOffsetScale(), PULLBACK_INTERPOLATOR);
             if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-                builder.addOnFrameCallback(
-                        () -> recentsView.redrawLiveTile(false /* mightNeedToRefill */));
+                builder.addOnFrameCallback(recentsView::redrawLiveTile);
             }
         } else if (mStartState == ALL_APPS) {
             AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
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 1b439d1..821ada4 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
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.uioverrides.touchcontrollers;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
@@ -47,6 +46,7 @@
 import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.PEEK;
 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.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.Animator;
@@ -244,7 +244,7 @@
         final LauncherState toState = OVERVIEW;
 
         // Set RecentView's initial properties.
-        SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]);
+        RECENTS_SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]);
         ADJACENT_PAGE_OFFSET.set(mRecentsView, 1f);
         mRecentsView.setContentAlpha(1);
         mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
@@ -266,7 +266,8 @@
         //   - RecentsView scale
         //   - RecentsView fullscreenProgress
         PendingAnimation yAnim = new PendingAnimation((long) (mYRange * 2));
-        yAnim.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0], SCALE_DOWN_INTERPOLATOR);
+        yAnim.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
+                SCALE_DOWN_INTERPOLATOR);
         yAnim.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
                 toState.getOverviewFullscreenProgress(), SCALE_DOWN_INTERPOLATOR);
         mYOverviewAnim = yAnim.createPlaybackController();
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 0ee5d04..fce019b 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
@@ -16,7 +16,6 @@
 package com.android.launcher3.uioverrides.touchcontrollers;
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE;
@@ -262,11 +261,6 @@
         mCurrentAnimation.setPlayFraction(Utilities.boundToRange(
                 totalDisplacement * mProgressMultiplier, 0, 1));
 
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            if (mRecentsView.getCurrentPage() != 0 || isGoingUp) {
-                mRecentsView.redrawLiveTile(true);
-            }
-        }
         return true;
     }
 
@@ -297,13 +291,6 @@
         }
 
         mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd, logAction));
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            mCurrentAnimation.getAnimationPlayer().addUpdateListener(valueAnimator -> {
-                if (mRecentsView.getCurrentPage() != 0 || mCurrentAnimationIsGoingUp) {
-                    mRecentsView.redrawLiveTile(true);
-                }
-            });
-        }
         mCurrentAnimation.startWithVelocity(mActivity, goingToEnd,
                 velocity, mEndDisplacement, animationDuration);
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AbsSwipeUpHandler.java
similarity index 81%
rename from quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
rename to quickstep/recents_ui_overrides/src/com/android/quickstep/AbsSwipeUpHandler.java
index 1e98a6b..3a5c027 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -15,6 +15,8 @@
  */
 package com.android.quickstep;
 
+import static android.widget.Toast.LENGTH_SHORT;
+
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
@@ -29,7 +31,9 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_LEFT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
 import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
 import static com.android.quickstep.GestureState.GestureEndTarget.NEW_TASK;
@@ -51,13 +55,17 @@
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.PointF;
+import android.graphics.Rect;
 import android.os.Build;
 import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnApplyWindowInsetsListener;
 import android.view.ViewTreeObserver.OnDrawListener;
 import android.view.WindowInsets;
 import android.view.animation.Interpolator;
+import android.widget.Toast;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
@@ -73,17 +81,24 @@
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.util.VibratorWrapper;
+import com.android.launcher3.util.WindowBounds;
 import com.android.quickstep.BaseActivityInterface.AnimationFactory;
 import com.android.quickstep.GestureState.GestureEndTarget;
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
+import com.android.quickstep.util.ActivityInitListener;
+import com.android.quickstep.util.InputConsumerProxy;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.ShelfPeekAnim;
 import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
+import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.views.LiveTileOverlay;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -95,17 +110,34 @@
 import com.android.systemui.shared.system.TaskInfoCompat;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
 /**
  * Handles the navigation gestures when Launcher is the default home activity.
- * TODO: Merge this with BaseSwipeUpHandler
  */
-@TargetApi(Build.VERSION_CODES.O)
-public abstract class BaseSwipeUpHandlerV2<T extends StatefulActivity<?>, Q extends RecentsView>
-        extends BaseSwipeUpHandler<T, Q> implements OnApplyWindowInsetsListener {
-    private static final String TAG = BaseSwipeUpHandlerV2.class.getSimpleName();
+@TargetApi(Build.VERSION_CODES.R)
+public abstract class AbsSwipeUpHandler<T extends StatefulActivity<?>, Q extends RecentsView>
+        extends SwipeUpAnimationLogic implements OnApplyWindowInsetsListener,
+        RecentsAnimationCallbacks.RecentsAnimationListener {
+    private static final String TAG = "AbsSwipeUpHandler";
 
     private static final String[] STATE_NAMES = DEBUG_STATES ? new String[16] : null;
 
+    protected final BaseActivityInterface<?, T> mActivityInterface;
+    protected final InputConsumerProxy mInputConsumerProxy;
+    protected final ActivityInitListener mActivityInitListener;
+    // Callbacks to be made once the recents animation starts
+    private final ArrayList<Runnable> mRecentsAnimationStartCallbacks = new ArrayList<>();
+    protected RecentsAnimationController mRecentsAnimationController;
+    protected RecentsAnimationTargets mRecentsAnimationTargets;
+    protected T mActivity;
+    protected Q mRecentsView;
+    protected Runnable mGestureEndCallback;
+    protected MultiStateCallback mStateCallback;
+    protected boolean mCanceled;
+    private boolean mRecentsViewScrollLinked = false;
+
     private static int getFlagForIndex(int index, String name) {
         if (DEBUG_STATES) {
             STATE_NAMES[index] = name;
@@ -201,11 +233,15 @@
 
     private final Runnable mOnDeferredActivityLaunch = this::onDeferredActivityLaunch;
 
-    public BaseSwipeUpHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
+    public AbsSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState,
             long touchTimeMs, boolean continuingLastGesture,
             InputConsumerController inputConsumer) {
-        super(context, deviceState, gestureState, inputConsumer);
+        super(context, deviceState, gestureState, new TransformParams());
+        mActivityInterface = gestureState.getActivityInterface();
+        mActivityInitListener = mActivityInterface.createActivityInitListener(this::onActivityInit);
+        mInputConsumerProxy =
+                new InputConsumerProxy(inputConsumer, this::createNewInputProxyHandler);
         mTaskAnimationManager = taskAnimationManager;
         mTouchTimeMs = touchTimeMs;
         mContinuingLastGesture = continuingLastGesture;
@@ -267,10 +303,6 @@
         mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED | STATE_RESUME_LAST_TASK,
                 this::notifyTransitionCancelled);
 
-        mGestureState.runOnceAtState(STATE_END_TARGET_SET,
-                () -> mDeviceState.onEndTargetCalculated(mGestureState.getEndTarget(),
-                        mActivityInterface));
-
         if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
             mStateCallback.addChangeListener(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT
                             | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT,
@@ -278,9 +310,17 @@
         }
     }
 
-    @Override
     protected boolean onActivityInit(Boolean alreadyOnHome) {
-        super.onActivityInit(alreadyOnHome);
+        T createdActivity = mActivityInterface.getCreatedActivity();
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.1");
+        }
+        if (createdActivity != null) {
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.2");
+            }
+            initTransitionEndpoints(createdActivity.getDeviceProfile());
+        }
         final T activity = mActivityInterface.getCreatedActivity();
         if (mActivity == activity) {
             return true;
@@ -319,7 +359,9 @@
         return true;
     }
 
-    @Override
+    /**
+     * Return true if the window should be translated horizontally if the recents view scrolls
+     */
     protected boolean moveWindowWithRecentsScroll() {
         return mGestureState.getEndTarget() != HOME;
     }
@@ -332,7 +374,7 @@
         if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
             return;
         }
-        mTaskViewSimulator.setRecentsConfiguration(mActivity.getResources().getConfiguration());
+        mTaskViewSimulator.setRecentsRotation(mActivity.getDisplay().getRotation());
 
         // If we've already ended the gesture and are going home, don't prepare recents UI,
         // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL.
@@ -395,6 +437,11 @@
         mGestureState.getActivityInterface().setOnDeferredActivityLaunchCallback(
                 mOnDeferredActivityLaunch);
 
+        mGestureState.runOnceAtState(STATE_END_TARGET_SET,
+                () -> mDeviceState.getRotationTouchHelper().
+                        onEndTargetCalculated(mGestureState.getEndTarget(),
+                                mActivityInterface));
+
         notifyGestureStartedAsync();
     }
 
@@ -443,7 +490,9 @@
                 .getHighResLoadingState().setVisible(true);
     }
 
-    @Override
+    /**
+     * Called when motion pause is detected
+     */
     public void onMotionPauseChanged(boolean isPaused) {
         setShelfState(isPaused ? PEEK : HIDE, ShelfPeekAnim.INTERPOLATOR, ShelfPeekAnim.DURATION);
     }
@@ -481,7 +530,6 @@
         mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
     }
 
-    @Override
     public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) {
         setIsLikelyToStartNewTask(isLikelyToStartNewTask, true /* animate */);
     }
@@ -537,11 +585,14 @@
         updateLauncherTransitionProgress();
     }
 
-    @Override
     public Intent getLaunchIntent() {
         return mGestureState.getOverviewIntent();
     }
 
+    /**
+     * Called when the value of {@link #mCurrentShift} changes
+     */
+    @UiThread
     @Override
     public void updateFinalShift() {
         final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
@@ -602,7 +653,41 @@
     public void onRecentsAnimationStart(RecentsAnimationController controller,
             RecentsAnimationTargets targets) {
         ActiveGestureLog.INSTANCE.addLog("startRecentsAnimationCallback", targets.apps.length);
-        super.onRecentsAnimationStart(controller, targets);
+        mRecentsAnimationController = controller;
+        mRecentsAnimationTargets = targets;
+        mTransformParams.setTargetSet(mRecentsAnimationTargets);
+        RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(
+                mGestureState.getRunningTaskId());
+
+        if (runningTaskTarget != null) {
+            mTaskViewSimulator.setPreview(runningTaskTarget);
+        }
+
+        // Only initialize the device profile, if it has not been initialized before, as in some
+        // configurations targets.homeContentInsets may not be correct.
+        if (mActivity == null) {
+            DeviceProfile dp = mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile();
+            if (targets.minimizedHomeBounds != null && runningTaskTarget != null) {
+                Rect overviewStackBounds = mActivityInterface
+                        .getOverviewWindowBounds(targets.minimizedHomeBounds, runningTaskTarget);
+                dp = dp.getMultiWindowProfile(mContext,
+                        new WindowBounds(overviewStackBounds, targets.homeContentInsets));
+            } else {
+                // If we are not in multi-window mode, home insets should be same as system insets.
+                dp = dp.copy(mContext);
+            }
+            dp.updateInsets(targets.homeContentInsets);
+            dp.updateIsSeascape(mContext);
+            initTransitionEndpoints(dp);
+        }
+
+        // Notify when the animation starts
+        if (!mRecentsAnimationStartCallbacks.isEmpty()) {
+            for (Runnable action : new ArrayList<>(mRecentsAnimationStartCallbacks)) {
+                action.run();
+            }
+            mRecentsAnimationStartCallbacks.clear();
+        }
 
         // Only add the callback to enable the input consumer after we actually have the controller
         mStateCallback.runOnceAtState(STATE_APP_CONTROLLER_RECEIVED | STATE_GESTURE_STARTED,
@@ -619,10 +704,14 @@
         mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
 
         // Defer clearing the controller and the targets until after we've updated the state
-        super.onRecentsAnimationCanceled(thumbnailData);
+        mRecentsAnimationController = null;
+        mRecentsAnimationTargets = null;
+        if (mRecentsView != null) {
+            mRecentsView.setRecentsAnimationTargets(null, null);
+        }
     }
 
-    @Override
+    @UiThread
     public void onGestureStarted(boolean isLikelyToStartNewTask) {
         notifyGestureStartedAsync();
         setIsLikelyToStartNewTask(isLikelyToStartNewTask, false /* animate */);
@@ -646,7 +735,7 @@
     /**
      * Called as a result on ACTION_CANCEL to return the UI to the start state.
      */
-    @Override
+    @UiThread
     public void onGestureCancelled() {
         updateDisplacement(0);
         mStateCallback.setStateOnUiThread(STATE_GESTURE_COMPLETED);
@@ -659,7 +748,7 @@
      * @param velocity The x and y components of the velocity when the gesture ends.
      * @param downPos The x and y value of where the gesture started.
      */
-    @Override
+    @UiThread
     public void onGestureEnded(float endVelocity, PointF velocity, PointF downPos) {
         float flingThreshold = mContext.getResources()
                 .getDimension(R.dimen.quickstep_fling_threshold_velocity);
@@ -677,7 +766,10 @@
         handleNormalGestureEnd(endVelocity, isFling, velocity, false /* isCancel */);
     }
 
-    @Override
+    /**
+     * Called to create a input proxy for the running task
+     */
+    @UiThread
     protected InputConsumer createNewInputProxyHandler() {
         endRunningWindowAnim(mGestureState.getEndTarget() == HOME /* cancel */);
         endLauncherTransitionController();
@@ -718,7 +810,7 @@
         ActiveGestureLog.INSTANCE.addLog("onSettledOnEndTarget " + mGestureState.getEndTarget());
     }
 
-    @Override
+    /** @return Whether this was the task we were waiting to appear, and thus handled it. */
     protected boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
         if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
             return false;
@@ -1095,10 +1187,12 @@
                 mActivityInterface.onSwipeUpToHomeComplete(mDeviceState);
             }
         });
+        if (mRecentsAnimationTargets != null) {
+            mRecentsAnimationTargets.addReleaseCheck(anim);
+        }
         return anim;
     }
 
-    @Override
     public void onConsumerAboutToBeSwitched() {
         if (mActivity != null) {
             // In the off chance that the gesture ends before Launcher is started, we should clear
@@ -1127,15 +1221,6 @@
 
     @UiThread
     private void startNewTask() {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            mRecentsAnimationController.finish(true /* toRecents */, this::startNewTaskInternal);
-        } else {
-            startNewTaskInternal();
-        }
-    }
-
-    @UiThread
-    private void startNewTaskInternal() {
         TaskView taskToLaunch = mRecentsView == null ? null : mRecentsView.getNextPageTaskView();
         startNewTask(success -> {
             if (!success) {
@@ -1149,9 +1234,19 @@
         });
     }
 
-    @Override
+    /**
+     * Called when we successfully startNewTask() on the task that was previously running. Normally
+     * we call resumeLastTask() when returning to the previously running task, but this handles a
+     * specific edge case: if we switch from A to B, and back to A before B appears, we need to
+     * start A again to ensure it stays on top.
+     */
+    @androidx.annotation.CallSuper
     protected void onRestartPreviouslyAppearedTask() {
-        super.onRestartPreviouslyAppearedTask();
+        // Finish the controller here, since we won't get onTaskAppeared() for a task that already
+        // appeared.
+        if (mRecentsAnimationController != null) {
+            mRecentsAnimationController.finish(false, null);
+        }
         reset();
     }
 
@@ -1256,7 +1351,7 @@
                     // new thumbnail
                     finishTransitionPosted = ViewUtils.postDraw(taskView,
                             () -> mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED),
-                                    this::isCanceled);
+                            this::isCanceled);
                 }
             }
             if (!finishTransitionPosted) {
@@ -1325,4 +1420,172 @@
         return app.isNotInRecents
                 || app.activityType == ACTIVITY_TYPE_HOME;
     }
+
+    /**
+     * To be called at the end of constructor of subclasses. This calls various methods which can
+     * depend on proper class initialization.
+     */
+    protected void initAfterSubclassConstructor() {
+        initTransitionEndpoints(
+                mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile());
+    }
+
+    protected void performHapticFeedback() {
+        VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
+    }
+
+    public Consumer<MotionEvent> getRecentsViewDispatcher(float navbarRotation) {
+        return mRecentsView != null ? mRecentsView.getEventDispatcher(navbarRotation) : null;
+    }
+
+    public void setGestureEndCallback(Runnable gestureEndCallback) {
+        mGestureEndCallback = gestureEndCallback;
+    }
+
+    protected void linkRecentsViewScroll() {
+        SurfaceTransactionApplier.create(mRecentsView, applier -> {
+            mTransformParams.setSyncTransactionApplier(applier);
+            runOnRecentsAnimationStart(() ->
+                    mRecentsAnimationTargets.addReleaseCheck(applier));
+        });
+
+        mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
+            if (moveWindowWithRecentsScroll()) {
+                updateFinalShift();
+            }
+        });
+        runOnRecentsAnimationStart(() ->
+                mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController,
+                        mRecentsAnimationTargets));
+        mRecentsViewScrollLinked = true;
+    }
+
+    protected void startNewTask(Consumer<Boolean> resultCallback) {
+        // Launch the task user scrolled to (mRecentsView.getNextPage()).
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            // We finish recents animation inside launchTask() when live tile is enabled.
+            mRecentsView.getNextPageTaskView().launchTask(false /* animate */,
+                    true /* freezeTaskList */);
+        } else {
+            if (!mCanceled) {
+                TaskView nextTask = mRecentsView.getNextPageTaskView();
+                if (nextTask != null) {
+                    int taskId = nextTask.getTask().key.id;
+                    mGestureState.updateLastStartedTaskId(taskId);
+                    boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds()
+                            .contains(taskId);
+                    nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
+                            success -> {
+                                resultCallback.accept(success);
+                                if (success) {
+                                    if (hasTaskPreviouslyAppeared) {
+                                        onRestartPreviouslyAppearedTask();
+                                    }
+                                } else {
+                                    mActivityInterface.onLaunchTaskFailed();
+                                    nextTask.notifyTaskLaunchFailed(TAG);
+                                    mRecentsAnimationController.finish(true /* toRecents */, null);
+                                }
+                            }, MAIN_EXECUTOR.getHandler());
+                } else {
+                    mActivityInterface.onLaunchTaskFailed();
+                    Toast.makeText(mContext, R.string.activity_not_available, LENGTH_SHORT).show();
+                    mRecentsAnimationController.finish(true /* toRecents */, null);
+                }
+            }
+            mCanceled = false;
+        }
+    }
+
+    /**
+     * Runs the given {@param action} if the recents animation has already started, or queues it to
+     * be run when it is next started.
+     */
+    protected void runOnRecentsAnimationStart(Runnable action) {
+        if (mRecentsAnimationTargets == null) {
+            mRecentsAnimationStartCallbacks.add(action);
+        } else {
+            action.run();
+        }
+    }
+
+    /**
+     * TODO can we remove this now that we don't finish the controller until onTaskAppeared()?
+     * @return whether the recents animation has started and there are valid app targets.
+     */
+    protected boolean hasTargets() {
+        return mRecentsAnimationTargets != null && mRecentsAnimationTargets.hasTargets();
+    }
+
+    @Override
+    public void onRecentsAnimationFinished(RecentsAnimationController controller) {
+        mRecentsAnimationController = null;
+        mRecentsAnimationTargets = null;
+        if (mRecentsView != null) {
+            mRecentsView.setRecentsAnimationTargets(null, null);
+        }
+    }
+
+    @Override
+    public void onTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
+        if (mRecentsAnimationController != null) {
+            if (handleTaskAppeared(appearedTaskTarget)) {
+                mRecentsAnimationController.finish(false /* toRecents */,
+                        null /* onFinishComplete */);
+                mActivityInterface.onLaunchTaskSuccess();
+                ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false);
+            }
+        }
+    }
+
+    /**
+     * @return The index of the TaskView in RecentsView whose taskId matches the task that will
+     * resume if we finish the controller.
+     */
+    protected int getLastAppearedTaskIndex() {
+        return mGestureState.getLastAppearedTaskId() != -1
+                ? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId())
+                : mRecentsView.getRunningTaskIndex();
+    }
+
+    /**
+     * @return Whether we are continuing a gesture that already landed on a new task,
+     * but before that task appeared.
+     */
+    protected boolean hasStartedNewTask() {
+        return mGestureState.getLastStartedTaskId() != -1;
+    }
+
+    /**
+     * Registers a callback to run when the activity is ready.
+     * @param intent The intent that will be used to start the activity if it doesn't exist already.
+     */
+    public void initWhenReady(Intent intent) {
+        // Preload the plan
+        RecentsModel.INSTANCE.get(mContext).getTasks(null);
+
+        mActivityInitListener.register(intent);
+    }
+
+    /**
+     * Applies the transform on the recents animation
+     */
+    protected void applyWindowTransform() {
+        if (mWindowTransitionController != null) {
+            float progress = mCurrentShift.value / mDragLengthFactor;
+            mWindowTransitionController.setPlayFraction(progress);
+        }
+        if (mRecentsAnimationTargets != null) {
+            if (mRecentsViewScrollLinked) {
+                mTaskViewSimulator.setScroll(mRecentsView.getScrollOffset());
+            }
+            mTaskViewSimulator.apply(mTransformParams);
+        }
+    }
+
+    public interface Factory {
+
+        AbsSwipeUpHandler<StatefulActivity<?>, RecentsView> newHandler(
+                GestureState gestureState, long touchTimeMs, boolean continuingLastGesture);
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
deleted file mode 100644
index aba5ab6..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ /dev/null
@@ -1,387 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.os.Build;
-import android.util.Log;
-import android.view.MotionEvent;
-
-import androidx.annotation.CallSuper;
-import androidx.annotation.UiThread;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.statemanager.StatefulActivity;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.util.VibratorWrapper;
-import com.android.launcher3.util.WindowBounds;
-import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
-import com.android.quickstep.util.ActiveGestureLog;
-import com.android.quickstep.util.ActivityInitListener;
-import com.android.quickstep.util.InputConsumerProxy;
-import com.android.quickstep.util.RectFSpringAnim;
-import com.android.quickstep.util.SurfaceTransactionApplier;
-import com.android.quickstep.util.TransformParams;
-import com.android.quickstep.views.RecentsView;
-import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.InputConsumerController;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-import java.util.ArrayList;
-import java.util.function.Consumer;
-
-/**
- * Base class for swipe up handler with some utility methods
- */
-@TargetApi(Build.VERSION_CODES.Q)
-public abstract class BaseSwipeUpHandler<T extends StatefulActivity<?>, Q extends RecentsView>
-        extends SwipeUpAnimationLogic implements RecentsAnimationListener {
-
-    private static final String TAG = "BaseSwipeUpHandler";
-
-    protected final BaseActivityInterface<?, T> mActivityInterface;
-    protected final InputConsumerProxy mInputConsumerProxy;
-
-    protected final ActivityInitListener mActivityInitListener;
-
-    protected RecentsAnimationController mRecentsAnimationController;
-    protected RecentsAnimationTargets mRecentsAnimationTargets;
-
-    // Callbacks to be made once the recents animation starts
-    private final ArrayList<Runnable> mRecentsAnimationStartCallbacks = new ArrayList<>();
-
-    protected T mActivity;
-    protected Q mRecentsView;
-
-    protected Runnable mGestureEndCallback;
-
-    protected MultiStateCallback mStateCallback;
-
-    protected boolean mCanceled;
-
-    private boolean mRecentsViewScrollLinked = false;
-
-    protected BaseSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
-            GestureState gestureState, InputConsumerController inputConsumer) {
-        super(context, deviceState, gestureState, new TransformParams());
-        mActivityInterface = gestureState.getActivityInterface();
-        mActivityInitListener = mActivityInterface.createActivityInitListener(this::onActivityInit);
-        mInputConsumerProxy =
-                new InputConsumerProxy(inputConsumer, this::createNewInputProxyHandler);
-    }
-
-    /**
-     * To be called at the end of constructor of subclasses. This calls various methods which can
-     * depend on proper class initialization.
-     */
-    protected void initAfterSubclassConstructor() {
-        initTransitionEndpoints(
-                mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile());
-    }
-
-    protected void performHapticFeedback() {
-        VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
-    }
-
-    public Consumer<MotionEvent> getRecentsViewDispatcher(float navbarRotation) {
-        return mRecentsView != null ? mRecentsView.getEventDispatcher(navbarRotation) : null;
-    }
-
-    public void setGestureEndCallback(Runnable gestureEndCallback) {
-        mGestureEndCallback = gestureEndCallback;
-    }
-
-    public abstract Intent getLaunchIntent();
-
-    protected void linkRecentsViewScroll() {
-        SurfaceTransactionApplier.create(mRecentsView, applier -> {
-            mTransformParams.setSyncTransactionApplier(applier);
-            runOnRecentsAnimationStart(() ->
-                    mRecentsAnimationTargets.addReleaseCheck(applier));
-        });
-
-        mRecentsView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
-            if (moveWindowWithRecentsScroll()) {
-                updateFinalShift();
-            }
-        });
-        runOnRecentsAnimationStart(() ->
-                mRecentsView.setRecentsAnimationTargets(mRecentsAnimationController,
-                        mRecentsAnimationTargets));
-        mRecentsViewScrollLinked = true;
-    }
-
-    protected void startNewTask(Consumer<Boolean> resultCallback) {
-        // Launch the task user scrolled to (mRecentsView.getNextPage()).
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            // We finish recents animation inside launchTask() when live tile is enabled.
-            mRecentsView.getNextPageTaskView().launchTask(false /* animate */,
-                    true /* freezeTaskList */);
-        } else {
-            int taskId = mRecentsView.getNextPageTaskView().getTask().key.id;
-            if (!mCanceled) {
-                TaskView nextTask = mRecentsView.getTaskView(taskId);
-                if (nextTask != null) {
-                    mGestureState.updateLastStartedTaskId(taskId);
-                    boolean hasTaskPreviouslyAppeared = mGestureState.getPreviouslyAppearedTaskIds()
-                            .contains(taskId);
-                    nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
-                            success -> {
-                                resultCallback.accept(success);
-                                if (success) {
-                                    if (hasTaskPreviouslyAppeared) {
-                                        onRestartPreviouslyAppearedTask();
-                                    }
-                                } else {
-                                    mActivityInterface.onLaunchTaskFailed();
-                                    nextTask.notifyTaskLaunchFailed(TAG);
-                                    mRecentsAnimationController.finish(true /* toRecents */, null);
-                                }
-                            }, MAIN_EXECUTOR.getHandler());
-                }
-            }
-            mCanceled = false;
-        }
-    }
-
-    /**
-     * Called when we successfully startNewTask() on the task that was previously running. Normally
-     * we call resumeLastTask() when returning to the previously running task, but this handles a
-     * specific edge case: if we switch from A to B, and back to A before B appears, we need to
-     * start A again to ensure it stays on top.
-     */
-    @CallSuper
-    protected void onRestartPreviouslyAppearedTask() {
-        // Finish the controller here, since we won't get onTaskAppeared() for a task that already
-        // appeared.
-        if (mRecentsAnimationController != null) {
-            mRecentsAnimationController.finish(false, null);
-        }
-    }
-
-    /**
-     * Runs the given {@param action} if the recents animation has already started, or queues it to
-     * be run when it is next started.
-     */
-    protected void runOnRecentsAnimationStart(Runnable action) {
-        if (mRecentsAnimationTargets == null) {
-            mRecentsAnimationStartCallbacks.add(action);
-        } else {
-            action.run();
-        }
-    }
-
-    /**
-     * TODO can we remove this now that we don't finish the controller until onTaskAppeared()?
-     * @return whether the recents animation has started and there are valid app targets.
-     */
-    protected boolean hasTargets() {
-        return mRecentsAnimationTargets != null && mRecentsAnimationTargets.hasTargets();
-    }
-
-    @Override
-    public void onRecentsAnimationStart(RecentsAnimationController recentsAnimationController,
-            RecentsAnimationTargets targets) {
-        mRecentsAnimationController = recentsAnimationController;
-        mRecentsAnimationTargets = targets;
-        mTransformParams.setTargetSet(mRecentsAnimationTargets);
-        RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(
-                mGestureState.getRunningTaskId());
-
-        if (runningTaskTarget != null) {
-            mTaskViewSimulator.setPreview(runningTaskTarget);
-        }
-
-        // Only initialize the device profile, if it has not been initialized before, as in some
-        // configurations targets.homeContentInsets may not be correct.
-        if (mActivity == null) {
-            DeviceProfile dp = mTaskViewSimulator.getOrientationState().getLauncherDeviceProfile();
-            if (targets.minimizedHomeBounds != null && runningTaskTarget != null) {
-                Rect overviewStackBounds = mActivityInterface
-                        .getOverviewWindowBounds(targets.minimizedHomeBounds, runningTaskTarget);
-                dp = dp.getMultiWindowProfile(mContext,
-                        new WindowBounds(overviewStackBounds, targets.homeContentInsets));
-            } else {
-                // If we are not in multi-window mode, home insets should be same as system insets.
-                dp = dp.copy(mContext);
-            }
-            dp.updateInsets(targets.homeContentInsets);
-            dp.updateIsSeascape(mContext);
-            initTransitionEndpoints(dp);
-        }
-
-        // Notify when the animation starts
-        if (!mRecentsAnimationStartCallbacks.isEmpty()) {
-            for (Runnable action : new ArrayList<>(mRecentsAnimationStartCallbacks)) {
-                action.run();
-            }
-            mRecentsAnimationStartCallbacks.clear();
-        }
-    }
-
-    @Override
-    public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
-        mRecentsAnimationController = null;
-        mRecentsAnimationTargets = null;
-        if (mRecentsView != null) {
-            mRecentsView.setRecentsAnimationTargets(null, null);
-        }
-    }
-
-    @Override
-    public void onRecentsAnimationFinished(RecentsAnimationController controller) {
-        mRecentsAnimationController = null;
-        mRecentsAnimationTargets = null;
-        if (mRecentsView != null) {
-            mRecentsView.setRecentsAnimationTargets(null, null);
-        }
-    }
-
-    @Override
-    public void onTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget) {
-        if (mRecentsAnimationController != null) {
-            if (handleTaskAppeared(appearedTaskTarget)) {
-                mRecentsAnimationController.finish(false /* toRecents */,
-                        null /* onFinishComplete */);
-                mActivityInterface.onLaunchTaskSuccess();
-                ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", false);
-            }
-        }
-    }
-
-    /** @return Whether this was the task we were waiting to appear, and thus handled it. */
-    protected abstract boolean handleTaskAppeared(RemoteAnimationTargetCompat appearedTaskTarget);
-
-    /**
-     * @return The index of the TaskView in RecentsView whose taskId matches the task that will
-     * resume if we finish the controller.
-     */
-    protected int getLastAppearedTaskIndex() {
-        return mGestureState.getLastAppearedTaskId() != -1
-                ? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId())
-                : mRecentsView.getRunningTaskIndex();
-    }
-
-    /**
-     * @return Whether we are continuing a gesture that already landed on a new task,
-     * but before that task appeared.
-     */
-    protected boolean hasStartedNewTask() {
-        return mGestureState.getLastStartedTaskId() != -1;
-    }
-
-    /**
-     * Return true if the window should be translated horizontally if the recents view scrolls
-     */
-    protected abstract boolean moveWindowWithRecentsScroll();
-
-    protected boolean onActivityInit(Boolean alreadyOnHome) {
-        T createdActivity = mActivityInterface.getCreatedActivity();
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.1");
-        }
-        if (createdActivity != null) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.PAUSE_NOT_DETECTED, "BaseSwipeUpHandler.2");
-            }
-            initTransitionEndpoints(createdActivity.getDeviceProfile());
-        }
-        return true;
-    }
-
-    /**
-     * Called to create a input proxy for the running task
-     */
-    @UiThread
-    protected abstract InputConsumer createNewInputProxyHandler();
-
-    /**
-     * Called when the value of {@link #mCurrentShift} changes
-     */
-    @UiThread
-    public abstract void updateFinalShift();
-
-    /**
-     * Called when motion pause is detected
-     */
-    public abstract void onMotionPauseChanged(boolean isPaused);
-
-    @UiThread
-    public void onGestureStarted(boolean isLikelyToStartNewTask) { }
-
-    @UiThread
-    public abstract void onGestureCancelled();
-
-    @UiThread
-    public abstract void onGestureEnded(float endVelocity, PointF velocity, PointF downPos);
-
-    public abstract void onConsumerAboutToBeSwitched();
-
-    public void setIsLikelyToStartNewTask(boolean isLikelyToStartNewTask) { }
-
-    /**
-     * Registers a callback to run when the activity is ready.
-     * @param intent The intent that will be used to start the activity if it doesn't exist already.
-     */
-    public void initWhenReady(Intent intent) {
-        // Preload the plan
-        RecentsModel.INSTANCE.get(mContext).getTasks(null);
-
-        mActivityInitListener.register(intent);
-    }
-
-    /**
-     * Applies the transform on the recents animation
-     */
-    protected void applyWindowTransform() {
-        if (mWindowTransitionController != null) {
-            float progress = mCurrentShift.value / mDragLengthFactor;
-            mWindowTransitionController.setPlayFraction(progress);
-        }
-        if (mRecentsAnimationTargets != null) {
-            if (mRecentsViewScrollLinked) {
-                mTaskViewSimulator.setScroll(mRecentsView.getScrollOffset());
-            }
-            mTaskViewSimulator.apply(mTransformParams);
-        }
-    }
-
-    @Override
-    protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
-            HomeAnimationFactory homeAnimationFactory) {
-        RectFSpringAnim anim =
-                super.createWindowAnimationToHome(startProgress, homeAnimationFactory);
-        if (mRecentsAnimationTargets != null) {
-            mRecentsAnimationTargets.addReleaseCheck(anim);
-        }
-        return anim;
-    }
-
-    public interface Factory {
-
-        BaseSwipeUpHandler newHandler(
-                GestureState gestureState, long touchTimeMs, boolean continuingLastGesture);
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
index 33b9cde..d1da0c1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
@@ -140,7 +140,7 @@
     }
 
     @Override
-    public void onExitOverview(RecentsAnimationDeviceState deviceState, Runnable exitRunnable) {
+    public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
         // no-op, fake landscape not supported for 3P
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
index fc7a119..ffb05df 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -15,14 +15,35 @@
  */
 package com.android.quickstep;
 
+import static android.content.Intent.EXTRA_COMPONENT_NAME;
+import static android.content.Intent.EXTRA_USER;
+
+import static com.android.launcher3.GestureNavContract.EXTRA_GESTURE_CONTRACT;
+import static com.android.launcher3.GestureNavContract.EXTRA_ICON_POSITION;
+import static com.android.launcher3.GestureNavContract.EXTRA_ICON_SURFACE;
+import static com.android.launcher3.GestureNavContract.EXTRA_REMOTE_CALLBACK;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
 
 import android.animation.ObjectAnimator;
+import android.annotation.TargetApi;
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.ParcelUuid;
+import android.os.UserHandle;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
 
 import androidx.annotation.NonNull;
 
@@ -32,18 +53,32 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.SpringAnimationBuilder;
 import com.android.quickstep.fallback.FallbackRecentsView;
+import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.util.TransformParams.BuilderProxy;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
 
+import java.lang.ref.WeakReference;
+import java.util.UUID;
+import java.util.function.Consumer;
+
 /**
  * Handles the navigation gestures when a 3rd party launcher is the default home activity.
  */
+@TargetApi(Build.VERSION_CODES.R)
 public class FallbackSwipeHandler extends
-        BaseSwipeUpHandlerV2<RecentsActivity, FallbackRecentsView> {
+        AbsSwipeUpHandler<RecentsActivity, FallbackRecentsView> {
+
+    /**
+     * Message used for receiving gesture nav contract information. We use a static messenger to
+     * avoid leaking too make binders in case the receiving launcher does not handle the contract
+     * properly.
+     */
+    private static StaticMessageReceiver sMessageReceiver = null;
 
     private FallbackHomeAnimationFactory mActiveAnimationFactory;
     private final boolean mRunningOverHome;
@@ -89,7 +124,9 @@
     protected HomeAnimationFactory createHomeAnimationFactory(long duration) {
         mActiveAnimationFactory = new FallbackHomeAnimationFactory(duration);
         ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
-        mContext.startActivity(new Intent(mGestureState.getHomeIntent()), options.toBundle());
+        Intent intent = new Intent(mGestureState.getHomeIntent());
+        mActiveAnimationFactory.addGestureContract(intent);
+        mContext.startActivity(intent, options.toBundle());
         return mActiveAnimationFactory;
     }
 
@@ -130,17 +167,20 @@
     }
 
     private class FallbackHomeAnimationFactory extends HomeAnimationFactory {
-
+        private final Rect mTempRect = new Rect();
         private final TransformParams mHomeAlphaParams = new TransformParams();
         private final AnimatedFloat mHomeAlpha;
 
         private final AnimatedFloat mVerticalShiftForScale = new AnimatedFloat();
-
         private final AnimatedFloat mRecentsAlpha = new AnimatedFloat();
 
+        private final RectF mTargetRect = new RectF();
+        private SurfaceControl mSurfaceControl;
+
         private final long mDuration;
+
+        private RectFSpringAnim mSpringAnim;
         FallbackHomeAnimationFactory(long duration) {
-            super(null);
             mDuration = duration;
 
             if (mRunningOverHome) {
@@ -162,6 +202,15 @@
                     this::updateRecentsActivityTransformDuringHomeAnim);
         }
 
+        @NonNull
+        @Override
+        public RectF getWindowTargetRect() {
+            if (mTargetRect.isEmpty()) {
+                mTargetRect.set(super.getWindowTargetRect());
+            }
+            return mTargetRect;
+        }
+
         private void updateRecentsActivityTransformDuringHomeAnim(SurfaceParams.Builder builder,
                 RemoteAnimationTargetCompat app, TransformParams params) {
             builder.withAlpha(mRecentsAlpha.value);
@@ -218,5 +267,87 @@
                         .start();
             }
         }
+
+        @Override
+        public void setAnimation(RectFSpringAnim anim) {
+            mSpringAnim = anim;
+        }
+
+        private void onMessageReceived(Message msg) {
+            try {
+                Bundle data = msg.getData();
+                RectF position = data.getParcelable(EXTRA_ICON_POSITION);
+                if (!position.isEmpty()) {
+                    mSurfaceControl = data.getParcelable(EXTRA_ICON_SURFACE);
+                    mTargetRect.set(position);
+                    if (mSpringAnim != null) {
+                        mSpringAnim.onTargetPositionChanged();
+                    }
+                }
+            } catch (Exception e) {
+                // Ignore
+            }
+        }
+
+        @Override
+        public void update(RectF currentRect, float progress, float radius) {
+            if (mSurfaceControl != null) {
+                currentRect.roundOut(mTempRect);
+                Transaction t = new Transaction();
+                t.setGeometry(mSurfaceControl, null, mTempRect, Surface.ROTATION_0);
+                t.apply();
+            }
+        }
+
+        private void addGestureContract(Intent intent) {
+            if (mRunningOverHome || mGestureState.getRunningTask() == null) {
+                return;
+            }
+
+            TaskKey key = new TaskKey(mGestureState.getRunningTask());
+            if (key.getComponent() != null) {
+                if (sMessageReceiver == null) {
+                    sMessageReceiver = new StaticMessageReceiver();
+                }
+
+                Bundle gestureNavContract = new Bundle();
+                gestureNavContract.putParcelable(EXTRA_COMPONENT_NAME, key.getComponent());
+                gestureNavContract.putParcelable(EXTRA_USER, UserHandle.of(key.userId));
+                gestureNavContract.putParcelable(EXTRA_REMOTE_CALLBACK,
+                        sMessageReceiver.newCallback(this::onMessageReceived));
+                intent.putExtra(EXTRA_GESTURE_CONTRACT, gestureNavContract);
+            }
+        }
+    }
+
+    private static class StaticMessageReceiver implements Handler.Callback {
+
+        private final Messenger mMessenger =
+                new Messenger(new Handler(Looper.getMainLooper(), this));
+
+        private ParcelUuid mCurrentUID = new ParcelUuid(UUID.randomUUID());
+        private WeakReference<Consumer<Message>> mCurrentCallback = new WeakReference<>(null);
+
+        public Message newCallback(Consumer<Message> callback) {
+            mCurrentUID = new ParcelUuid(UUID.randomUUID());
+            mCurrentCallback = new WeakReference<>(callback);
+
+            Message msg = Message.obtain();
+            msg.replyTo = mMessenger;
+            msg.obj = mCurrentUID;
+            return msg;
+        }
+
+        @Override
+        public boolean handleMessage(@NonNull Message message) {
+            if (mCurrentUID.equals(message.obj)) {
+                Consumer<Message> consumer = mCurrentCallback.get();
+                if (consumer != null) {
+                    consumer.accept(message);
+                    return true;
+                }
+            }
+            return false;
+        }
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
index 6621b41..5a35eb5 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -41,7 +41,6 @@
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statehandlers.DepthController.ClampedDepthProperty;
 import com.android.launcher3.statemanager.StateManager;
@@ -105,7 +104,7 @@
         // recents, we assume the first task is invisible, making translation off by one task.
         launcher.getStateManager().reapplyState();
         launcher.getRootView().setForceHideBackArrow(false);
-        notifyRecentsOfOrientation(deviceState);
+        notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
     }
 
     @Override
@@ -120,7 +119,7 @@
     @Override
     public AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
             boolean activityVisible, Consumer<AnimatorPlaybackController> callback) {
-        notifyRecentsOfOrientation(deviceState);
+        notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
         DefaultAnimationFactory factory = new DefaultAnimationFactory(callback) {
             @Override
             public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator,
@@ -228,7 +227,7 @@
 
 
     @Override
-    public void onExitOverview(RecentsAnimationDeviceState deviceState, Runnable exitRunnable) {
+    public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
         final StateManager<LauncherState> stateManager = getCreatedActivity().getStateManager();
         stateManager.addStateListener(
                 new StateManager.StateListener<LauncherState>() {
@@ -244,11 +243,11 @@
                 });
     }
 
-    private void notifyRecentsOfOrientation(RecentsAnimationDeviceState deviceState) {
+    private void notifyRecentsOfOrientation(RotationTouchHelper rotationTouchHelper) {
         // reset layout on swipe to home
         RecentsView recentsView = getCreatedActivity().getOverviewPanel();
-        recentsView.setLayoutRotation(deviceState.getCurrentActiveRotation(),
-                deviceState.getDisplayRotation());
+        recentsView.setLayoutRotation(rotationTouchHelper.getCurrentActiveRotation(),
+                rotationTouchHelper.getDisplayRotation());
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index fa7d268..4411455 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -16,6 +16,7 @@
 package com.android.quickstep;
 
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 
 import android.animation.AnimatorSet;
 import android.content.Context;
@@ -28,6 +29,7 @@
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.views.FloatingIconView;
+import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.StaggeredWorkspaceAnim;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -37,7 +39,7 @@
  * Temporary class to allow easier refactoring
  */
 public class LauncherSwipeHandlerV2 extends
-        BaseSwipeUpHandlerV2<BaseQuickstepLauncher, RecentsView> {
+        AbsSwipeUpHandler<BaseQuickstepLauncher, RecentsView> {
 
     public LauncherSwipeHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
             TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
@@ -72,36 +74,39 @@
             mActivity.getRootView().setForceHideBackArrow(true);
             mActivity.setHintUserWillBeActive();
 
-            homeAnimFactory = new HomeAnimationFactory(floatingIconView) {
-
-                @Override
-                public RectF getWindowTargetRect() {
-                    if (canUseWorkspaceView) {
+            if (canUseWorkspaceView) {
+                // We want the window alpha to be 0 once this threshold is met, so that the
+                // FolderIconView can be seen morphing into the icon shape.
+                float windowAlphaThreshold = 1f - SHAPE_PROGRESS_DURATION;
+                homeAnimFactory = new LauncherHomeAnimationFactory() {
+                    @Override
+                    public RectF getWindowTargetRect() {
                         return iconLocation;
-                    } else {
-                        return super.getWindowTargetRect();
                     }
-                }
 
-                @NonNull
-                @Override
-                public AnimatorPlaybackController createActivityAnimationToHome() {
-                    // Return an empty APC here since we have an non-user controlled animation
-                    // to home.
-                    long accuracy = 2 * Math.max(mDp.widthPx, mDp.heightPx);
-                    return mActivity.getStateManager().createAnimationToNewWorkspace(
-                            NORMAL, accuracy, 0 /* animComponents */);
-                }
+                    @Override
+                    public void setAnimation(RectFSpringAnim anim) {
+                        anim.addAnimatorListener(floatingIconView);
+                        floatingIconView.setOnTargetChangeListener(anim::onTargetPositionChanged);
+                        floatingIconView.setFastFinishRunnable(anim::end);
+                    }
 
-                @Override
-                public void playAtomicAnimation(float velocity) {
-                    new StaggeredWorkspaceAnim(mActivity, velocity,
-                            true /* animateOverviewScrim */).start();
-                }
-            };
+                    @Override
+                    public void update(RectF currentRect, float progress, float radius) {
+                        floatingIconView.update(currentRect, 1f, progress, windowAlphaThreshold,
+                                radius, false);
+                    }
 
+                    @Override
+                    public void onCancel() {
+                        floatingIconView.fastFinish();
+                    }
+                };
+            } else {
+                homeAnimFactory = new LauncherHomeAnimationFactory();
+            }
         } else {
-            homeAnimFactory = new HomeAnimationFactory(null) {
+            homeAnimFactory = new HomeAnimationFactory() {
                 @Override
                 public AnimatorPlaybackController createActivityAnimationToHome() {
                     return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
@@ -118,4 +123,22 @@
         mRecentsAnimationController.finish(
                 true /* toRecents */, callback, true /* sendUserLeaveHint */);
     }
+
+    private class LauncherHomeAnimationFactory extends HomeAnimationFactory {
+        @NonNull
+        @Override
+        public AnimatorPlaybackController createActivityAnimationToHome() {
+            // Return an empty APC here since we have an non-user controlled animation
+            // to home.
+            long accuracy = 2 * Math.max(mDp.widthPx, mDp.heightPx);
+            return mActivity.getStateManager().createAnimationToNewWorkspace(
+                    NORMAL, accuracy, 0 /* animComponents */);
+        }
+
+        @Override
+        public void playAtomicAnimation(float velocity) {
+            new StaggeredWorkspaceAnim(mActivity, velocity,
+                    true /* animateOverviewScrim */).start();
+        }
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 6f4d34c..5026f36 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -58,6 +58,12 @@
                         FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get());
                 return response;
             }
+
+            case TestProtocol.REQUEST_OVERVIEW_SHARE_ENABLED: {
+                response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                        FeatureFlags.ENABLE_OVERVIEW_SHARE.get());
+                return response;
+            }
         }
 
         return super.call(method);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java
index dc8f1c5..e54a21c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -17,7 +17,6 @@
 
 import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 
 import android.animation.Animator;
 import android.content.Context;
@@ -28,7 +27,6 @@
 import android.view.animation.Interpolator;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
 import com.android.launcher3.DeviceProfile;
@@ -37,7 +35,6 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.views.FloatingIconView;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.util.TransformParams;
@@ -85,7 +82,8 @@
         mTransformParams = transformParams;
 
         mTaskViewSimulator.setLayoutRotation(
-                mDeviceState.getCurrentActiveRotation(), mDeviceState.getDisplayRotation());
+                mDeviceState.getRotationTouchHelper().getCurrentActiveRotation(),
+                mDeviceState.getRotationTouchHelper().getDisplayRotation());
     }
 
     protected void initTransitionEndpoints(DeviceProfile dp) {
@@ -148,12 +146,6 @@
 
     protected abstract class HomeAnimationFactory {
 
-        public FloatingIconView mIconView;
-
-        public HomeAnimationFactory(@Nullable FloatingIconView iconView) {
-            mIconView = iconView;
-        }
-
         public @NonNull RectF getWindowTargetRect() {
             PagedOrientationHandler orientationHandler = getOrientationHandler();
             DeviceProfile dp = mDp;
@@ -174,6 +166,12 @@
         public void playAtomicAnimation(float velocity) {
             // No-op
         }
+
+        public void setAnimation(RectFSpringAnim anim) { }
+
+        public void update(RectF currentRect, float progress, float radius) { }
+
+        public void onCancel() { }
     }
 
     /**
@@ -184,8 +182,6 @@
     protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
             HomeAnimationFactory homeAnimationFactory) {
         final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
-        final FloatingIconView fiv = homeAnimationFactory.mIconView;
-        final boolean isFloatingIconView = fiv != null;
 
         mWindowTransitionController.setPlayFraction(startProgress / mDragLengthFactor);
         mTaskViewSimulator.apply(mTransformParams.setProgress(startProgress));
@@ -203,11 +199,7 @@
         windowToHomePositionMap.mapRect(startRect);
 
         RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mContext);
-        if (isFloatingIconView) {
-            anim.addAnimatorListener(fiv);
-            fiv.setOnTargetChangeListener(anim::onTargetPositionChanged);
-            fiv.setFastFinishRunnable(anim::end);
-        }
+        homeAnimationFactory.setAnimation(anim);
 
         SpringAnimationRunner runner = new SpringAnimationRunner(
                 homeAnimationFactory, cropRectF, homeToWindowPositionMap);
@@ -242,32 +234,27 @@
 
         final RectF mWindowCurrentRect = new RectF();
         final Matrix mHomeToWindowPositionMap;
+        final HomeAnimationFactory mAnimationFactory;
 
-        final FloatingIconView mFIV;
         final AnimatorPlaybackController mHomeAnim;
         final RectF mCropRectF;
 
         final float mStartRadius;
         final float mEndRadius;
-        final float mWindowAlphaThreshold;
 
         SpringAnimationRunner(HomeAnimationFactory factory, RectF cropRectF,
                 Matrix homeToWindowPositionMap) {
+            mAnimationFactory = factory;
             mHomeAnim = factory.createActivityAnimationToHome();
             mCropRectF = cropRectF;
             mHomeToWindowPositionMap = homeToWindowPositionMap;
 
             cropRectF.roundOut(mCropRect);
-            mFIV = factory.mIconView;
 
             // End on a "round-enough" radius so that the shape reveal doesn't have to do too much
             // rounding at the end of the animation.
             mStartRadius = mTaskViewSimulator.getCurrentCornerRadius();
             mEndRadius = cropRectF.width() / 2f;
-
-            // We want the window alpha to be 0 once this threshold is met, so that the
-            // FolderIconView can be seen morphing into the icon shape.
-            mWindowAlphaThreshold = mFIV != null ? 1f - SHAPE_PROGRESS_DURATION : 1f;
         }
 
         @Override
@@ -282,10 +269,7 @@
                     .setCornerRadius(cornerRadius);
 
             mTransformParams.applySurfaceParams(mTransformParams.createSurfaceParams(this));
-            if (mFIV != null) {
-                mFIV.update(currentRect, 1f, progress,
-                        mWindowAlphaThreshold, mMatrix.mapRadius(cornerRadius), false);
-            }
+            mAnimationFactory.update(currentRect, progress, mMatrix.mapRadius(cornerRadius));
         }
 
         @Override
@@ -298,9 +282,7 @@
 
         @Override
         public void onCancel() {
-            if (mFIV != null) {
-                mFIV.fastFinish();
-            }
+            mAnimationFactory.onCancel();
         }
 
         @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
index e9614d1..db512fa 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
@@ -19,6 +19,7 @@
 import static android.view.Surface.ROTATION_0;
 
 import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
+import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL;
 import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
 
 import android.annotation.SuppressLint;
@@ -146,26 +147,29 @@
          */
         public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
                 boolean rotated) {
-            final boolean isAllowedByPolicy = thumbnail.isRealSnapshot;
+            getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null);
 
-            getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
+            if (thumbnail != null) {
+                getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
+                final boolean isAllowedByPolicy = thumbnail.isRealSnapshot;
 
-            getActionsView().setCallbacks(new OverlayUICallbacks() {
-                @Override
-                public void onShare() {
-                    if (isAllowedByPolicy) {
-                        mImageApi.startShareActivity();
-                    } else {
-                        showBlockedByPolicyMessage();
+                getActionsView().setCallbacks(new OverlayUICallbacks() {
+                    @Override
+                    public void onShare() {
+                        if (isAllowedByPolicy) {
+                            mImageApi.startShareActivity();
+                        } else {
+                            showBlockedByPolicyMessage();
+                        }
                     }
-                }
 
-                @SuppressLint("NewApi")
-                @Override
-                public void onScreenshot() {
-                    saveScreenshot(task);
-                }
-            });
+                    @SuppressLint("NewApi")
+                    @Override
+                    public void onScreenshot() {
+                        saveScreenshot(task);
+                    }
+                });
+            }
         }
 
         /**
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 131ebc1..9d16f0e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -259,6 +259,7 @@
 
     private static boolean sConnected = false;
     private static boolean sIsInitialized = false;
+    private RotationTouchHelper mRotationTouchHelper;
 
     public static boolean isConnected() {
         return sConnected;
@@ -268,9 +269,9 @@
         return sIsInitialized;
     }
 
-    private final BaseSwipeUpHandler.Factory mLauncherSwipeHandlerFactory =
+    private final AbsSwipeUpHandler.Factory mLauncherSwipeHandlerFactory =
             this::createLauncherSwipeHandler;
-    private final BaseSwipeUpHandler.Factory mFallbackSwipeHandlerFactory =
+    private final AbsSwipeUpHandler.Factory mFallbackSwipeHandlerFactory =
             this::createFallbackSwipeHandler;
 
     private ActivityManagerWrapper mAM;
@@ -300,6 +301,7 @@
         mDeviceState.addNavigationModeChangedCallback(this::onNavigationModeChanged);
         mDeviceState.addOneHandedModeChangedCallback(this::onOneHandedModeOverlayChanged);
         mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
+        mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
         ProtoTracer.INSTANCE.get(this).add(this);
 
         sConnected = true;
@@ -328,7 +330,7 @@
         mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(),
                 mMainChoreographer, this::onInputEvent);
 
-        mDeviceState.updateGestureTouchRegions();
+        mRotationTouchHelper.updateGestureTouchRegions();
     }
 
     /**
@@ -479,9 +481,9 @@
             if (TestProtocol.sDebugTracing) {
                 Log.d(TestProtocol.NO_SWIPE_TO_HOME, "TouchInteractionService.onInputEvent:DOWN");
             }
-            mDeviceState.setOrientationTransformIfNeeded(event);
+            mRotationTouchHelper.setOrientationTransformIfNeeded(event);
 
-            if (mDeviceState.isInSwipeUpTouchRegion(event)) {
+            if (mRotationTouchHelper.isInSwipeUpTouchRegion(event)) {
                 if (TestProtocol.sDebugTracing) {
                     Log.d(TestProtocol.NO_SWIPE_TO_HOME,
                             "TouchInteractionService.onInputEvent:isInSwipeUpTouchRegion");
@@ -508,6 +510,11 @@
                             mGestureState,
                             InputConsumer.NO_OP, mInputMonitorCompat,
                             mOverviewComponentObserver.assistantGestureIsConstrained());
+                } else if (mDeviceState.canTriggerOneHandedAction(event)
+                    && !mDeviceState.isOneHandedModeActive()) {
+                    // Consume gesture event for triggering one handed feature.
+                    mUncheckedConsumer = new OneHandedModeInputConsumer(this, mDeviceState,
+                        InputConsumer.NO_OP, mInputMonitorCompat);
                 } else {
                     mUncheckedConsumer = InputConsumer.NO_OP;
                 }
@@ -523,7 +530,7 @@
             // Other events
             if (mUncheckedConsumer != InputConsumer.NO_OP) {
                 // Only transform the event if we are handling it in a proper consumer
-                mDeviceState.setOrientationTransformIfNeeded(event);
+                mRotationTouchHelper.setOrientationTransformIfNeeded(event);
             }
         }
 
@@ -561,7 +568,7 @@
             gestureState.updatePreviouslyAppearedTaskIds(
                     previousGestureState.getPreviouslyAppearedTaskIds());
         } else {
-            gestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.0",
+            gestureState.updateRunningTask(TraceHelper.allowIpcs("getRunningTask.0",
                     () -> mAM.getRunningTask(false /* filterOnlyVisibleRecents */)));
         }
         return gestureState;
@@ -684,7 +691,7 @@
         if (AssistantUtilities.isExcludedAssistant(gestureState.getRunningTask())) {
             // In the case where we are in the excluded assistant state, ignore it and treat the
             // running activity as the task behind the assistant
-            gestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.assistant",
+            gestureState.updateRunningTask(TraceHelper.allowIpcs("getRunningTask.assistant",
                     () -> mAM.getRunningTask(true /* filterOnlyVisibleRecents */)));
             ComponentName homeComponent = mOverviewComponentObserver.getHomeIntent().getComponent();
             ComponentName runningComponent =
@@ -714,7 +721,7 @@
     private InputConsumer createOtherActivityInputConsumer(GestureState gestureState,
             MotionEvent event) {
 
-        final BaseSwipeUpHandler.Factory factory;
+        final AbsSwipeUpHandler.Factory factory;
         if (!mOverviewComponentObserver.isHomeAndOverviewSame()) {
             factory = mFallbackSwipeHandlerFactory;
         } else {
@@ -887,13 +894,13 @@
         }
     }
 
-    private BaseSwipeUpHandler createLauncherSwipeHandler(
+    private AbsSwipeUpHandler createLauncherSwipeHandler(
             GestureState gestureState, long touchTimeMs, boolean continuingLastGesture) {
         return new LauncherSwipeHandlerV2(this, mDeviceState, mTaskAnimationManager,
                 gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
     }
 
-    private BaseSwipeUpHandler createFallbackSwipeHandler(
+    private AbsSwipeUpHandler createFallbackSwipeHandler(
             GestureState gestureState, long touchTimeMs, boolean continuingLastGesture) {
         return new FallbackSwipeHandler(this, mDeviceState, mTaskAnimationManager,
                 gestureState, touchTimeMs, continuingLastGesture, mInputConsumer);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index 3f1e7ba..163c232 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -15,7 +15,6 @@
  */
 package com.android.quickstep.fallback;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
@@ -25,6 +24,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
 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_MODALNESS;
 
 import com.android.launcher3.anim.PendingAnimation;
@@ -82,7 +82,7 @@
                 MultiValueAlpha.VALUE, buttonAlpha, LINEAR);
 
         float[] scaleAndOffset = state.getOverviewScaleAndOffset(mActivity);
-        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0],
+        setter.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
                 config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
         setter.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1],
                 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
index 5ad48eb..0c2c92c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
@@ -99,7 +99,8 @@
             case ACTION_POINTER_DOWN: {
                 if (mState == STATE_INACTIVE) {
                     int pointerIndex = ev.getActionIndex();
-                    if (mDeviceState.isInSwipeUpTouchRegion(ev, pointerIndex)
+                    if (mDeviceState.getRotationTouchHelper()
+                            .isInSwipeUpTouchRegion(ev, pointerIndex)
                             && mDelegate.allowInterceptByParent()) {
                         setActive(ev);
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
index 3a97216..db1948b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DeviceLockedInputConsumer.java
@@ -21,7 +21,7 @@
 
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.Utilities.squaredTouchSlop;
-import static com.android.quickstep.BaseSwipeUpHandlerV2.MIN_PROGRESS_FOR_OVERVIEW;
+import static com.android.quickstep.AbsSwipeUpHandler.MIN_PROGRESS_FOR_OVERVIEW;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 
@@ -147,7 +147,7 @@
                 if (!mThresholdCrossed) {
                     // Cancel interaction in case of multi-touch interaction
                     int ptrIdx = ev.getActionIndex();
-                    if (!mDeviceState.isInSwipeUpTouchRegion(ev, ptrIdx)) {
+                    if (!mDeviceState.getRotationTouchHelper().isInSwipeUpTouchRegion(ev, ptrIdx)) {
                         int action = ev.getAction();
                         ev.setAction(ACTION_CANCEL);
                         finishTouchTracking(ev);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
index 0bb8fff..22bd334 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
@@ -52,7 +52,6 @@
 
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
-    private final PointF mStartDragPos = new PointF();
 
     private boolean mPassedSlop;
 
@@ -92,7 +91,6 @@
                 if (!mPassedSlop) {
                     if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
                             > mSquaredSlop) {
-                        mStartDragPos.set(mLastPos.x, mLastPos.y);
                         if ((!mDeviceState.isOneHandedModeActive() && isValidStartAngle(
                                 mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y))
                                 || (mDeviceState.isOneHandedModeActive() && isValidExitAngle(
@@ -104,17 +102,16 @@
                         }
                     }
                 } else {
-                    float distance = (float) Math.hypot(mLastPos.x - mStartDragPos.x,
-                            mLastPos.y - mStartDragPos.y);
+                    float distance = (float) Math.hypot(mLastPos.x - mDownPos.x,
+                            mLastPos.y - mDownPos.y);
                     if (distance > mDragDistThreshold && mPassedSlop) {
                         onStopGestureDetected();
                     }
                 }
                 break;
             }
-            case ACTION_UP:
-            case ACTION_CANCEL: {
-                if (mLastPos.y >= mStartDragPos.y && mPassedSlop) {
+            case ACTION_UP: {
+                if (mLastPos.y >= mDownPos.y && mPassedSlop) {
                     onStartGestureDetected();
                 }
 
@@ -122,6 +119,10 @@
                 mState = STATE_INACTIVE;
                 break;
             }
+            case ACTION_CANCEL:
+                mPassedSlop = false;
+                mState = STATE_INACTIVE;
+                break;
         }
 
         if (mState != STATE_ACTIVE) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 26df9c7..f02e5e6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -53,12 +53,13 @@
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.BaseActivityInterface;
-import com.android.quickstep.BaseSwipeUpHandler;
-import com.android.quickstep.BaseSwipeUpHandler.Factory;
+import com.android.quickstep.AbsSwipeUpHandler;
+import com.android.quickstep.AbsSwipeUpHandler.Factory;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
 import com.android.quickstep.RecentsAnimationCallbacks;
 import com.android.quickstep.RecentsAnimationDeviceState;
+import com.android.quickstep.RotationTouchHelper;
 import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.CachedEventDispatcher;
@@ -86,12 +87,13 @@
     private final NavBarPosition mNavBarPosition;
     private final TaskAnimationManager mTaskAnimationManager;
     private final GestureState mGestureState;
+    private final RotationTouchHelper mRotationTouchHelper;
     private RecentsAnimationCallbacks mActiveCallbacks;
     private final CachedEventDispatcher mRecentsViewDispatcher = new CachedEventDispatcher();
     private final InputMonitorCompat mInputMonitorCompat;
     private final BaseActivityInterface mActivityInterface;
 
-    private final BaseSwipeUpHandler.Factory mHandlerFactory;
+    private final AbsSwipeUpHandler.Factory mHandlerFactory;
 
     private final Consumer<OtherActivityInputConsumer> mOnCompleteCallback;
     private final MotionPauseDetector mMotionPauseDetector;
@@ -99,7 +101,7 @@
 
     private VelocityTracker mVelocityTracker;
 
-    private BaseSwipeUpHandler mInteractionHandler;
+    private AbsSwipeUpHandler mInteractionHandler;
 
     private final boolean mIsDeferredDownTarget;
     private final PointF mDownPos = new PointF();
@@ -163,6 +165,7 @@
 
         mPassedPilferInputSlop = mPassedWindowMoveSlop = continuingPreviousGesture;
         mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
+        mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
     }
 
     @Override
@@ -230,7 +233,7 @@
                 if (!mPassedPilferInputSlop) {
                     // Cancel interaction in case of multi-touch interaction
                     int ptrIdx = ev.getActionIndex();
-                    if (!mDeviceState.isInSwipeUpTouchRegion(ev, ptrIdx)) {
+                    if (!mRotationTouchHelper.isInSwipeUpTouchRegion(ev, ptrIdx)) {
                         forceCancelGesture(ev);
                     }
                 }
@@ -424,7 +427,7 @@
 
     @Override
     public void notifyOrientationSetup() {
-        mDeviceState.onStartGesture();
+        mRotationTouchHelper.onStartGesture();
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/logging/UserEventDispatcherAppPredictionExtension.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/logging/UserEventDispatcherAppPredictionExtension.java
deleted file mode 100644
index e000803..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/logging/UserEventDispatcherAppPredictionExtension.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.logging;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.appprediction.PredictionUiStateManager;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-
-import java.util.ArrayList;
-
-/**
- * This class handles AOSP MetricsLogger function calls and logging around
- * quickstep interactions and app launches.
- */
-@SuppressWarnings("unused")
-public class UserEventDispatcherAppPredictionExtension extends UserEventDispatcherExtension {
-
-    public static final int ALL_APPS_PREDICTION_TIPS = 2;
-
-    private static final String TAG = "UserEventDispatcher";
-
-    public UserEventDispatcherAppPredictionExtension(Context context) {
-        super(context);
-    }
-
-    @Override
-    protected void onFillInLogContainerData(
-            @NonNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target,
-            @NonNull ArrayList<LauncherLogProto.Target> targets) {
-        PredictionUiStateManager.fillInPredictedRank(itemInfo, target);
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
index 64ae1e0..94e496d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -32,7 +32,6 @@
 import android.util.IntProperty;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.PagedOrientationHandler;
@@ -73,6 +72,7 @@
     private final BaseActivityInterface mSizeStrategy;
 
     private final Rect mTaskRect = new Rect();
+    private float mOffsetY;
     private final PointF mPivot = new PointF();
     private DeviceProfile mDp;
 
@@ -93,7 +93,6 @@
     public final AnimatedFloat recentsViewScale = new AnimatedFloat();
     public final AnimatedFloat fullScreenProgress = new AnimatedFloat();
     private final ScrollState mScrollState = new ScrollState();
-    private final int mPageSpacing;
 
     // Cached calculations
     private boolean mLayoutValid = false;
@@ -107,7 +106,6 @@
         mOrientationState.setGestureActive(true);
 
         mCurrentFullscreenParams = new FullscreenDrawParams(context);
-        mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
     }
 
     /**
@@ -130,8 +128,8 @@
     /**
      * @see com.android.quickstep.views.RecentsView#onConfigurationChanged(Configuration)
      */
-    public void setRecentsConfiguration(Configuration configuration) {
-        mOrientationState.setActivityConfiguration(configuration);
+    public void setRecentsRotation(int recentsRotation) {
+        mOrientationState.setRecentsRotation(recentsRotation);
         mLayoutValid = false;
     }
 
@@ -178,8 +176,12 @@
         }
     }
 
+    public void setOffsetY(float offsetY) {
+        mOffsetY = offsetY;
+    }
+
     /**
-     * Adds animation for all the components corresponding to transition from an app to overview
+     * Adds animation for all the components corresponding to transition from an app to overview.
      */
     public void addAppToOverviewAnim(PendingAnimation pa, TimeInterpolator interpolator) {
         pa.addFloat(fullScreenProgress, AnimatedFloat.VALUE, 1, 0, interpolator);
@@ -187,6 +189,14 @@
     }
 
     /**
+     * Adds animation for all the components corresponding to transition from overview to the app.
+     */
+    public void addOverviewToAppAnim(PendingAnimation pa, TimeInterpolator interpolator) {
+        pa.addFloat(fullScreenProgress, AnimatedFloat.VALUE, 0, 1, interpolator);
+        pa.addFloat(recentsViewScale, AnimatedFloat.VALUE, 1, getFullScreenScale(), interpolator);
+    }
+
+    /**
      * Returns the current clipped/visible window bounds in the window coordinate space
      */
     public RectF getCurrentCropRect() {
@@ -262,7 +272,8 @@
             int start = mOrientationState.getOrientationHandler()
                     .getPrimaryValue(mTaskRect.left, mTaskRect.top);
             mScrollState.screenCenter = start + mScrollState.scroll + mScrollState.halfPageSize;
-            mScrollState.updateInterpolation(start, mPageSpacing);
+            mScrollState.pageParentScale = recentsViewScale.value;
+            mScrollState.updateInterpolation(start);
             mCurveScale = TaskView.getCurveScaleForInterpolation(mScrollState.linearInterpolation);
         }
 
@@ -281,7 +292,7 @@
         mMatrix.postScale(scale, scale);
 
         // Apply TaskView matrix: translate, scale, scroll
-        mMatrix.postTranslate(mTaskRect.left, mTaskRect.top);
+        mMatrix.postTranslate(mTaskRect.left, mTaskRect.top + mOffsetY);
         mMatrix.postScale(mCurveScale, mCurveScale, taskWidth / 2, taskHeight / 2);
         mOrientationState.getOrientationHandler().set(
                 mMatrix, MATRIX_POST_TRANSLATE, mScrollState.scroll);
@@ -307,7 +318,7 @@
                 .withCornerRadius(getCurrentCornerRadius());
 
         if (ENABLE_QUICKSTEP_LIVE_TILE.get() && params.getRecentsSurface() != null) {
-            builder.withRelativeLayerTo(params.getRecentsSurface(), Integer.MAX_VALUE);
+            builder.withRelativeLayerTo(params.getRecentsSurface(), Integer.MIN_VALUE);
         }
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java
index 3d44eb6..c06dd9c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java
@@ -54,6 +54,7 @@
 
 /**
  * View used to educate the user on how to access All Apps when in No Nav Button navigation mode.
+ * Consumes all touches until after the animation is completed and the view is removed.
  */
 public class AllAppsEduView extends AbstractFloatingView {
 
@@ -114,8 +115,18 @@
     }
 
     @Override
+    public boolean onBackPressed() {
+        return true;
+    }
+
+    @Override
+    public boolean canInterceptEventsInSystemGestureRegion() {
+        return true;
+    }
+
+    @Override
     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        return mAnimation != null && mAnimation.isRunning();
+        return true;
     }
 
     private void playAnimation() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
index 1034234..f31bc19 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -15,7 +15,6 @@
  */
 package com.android.quickstep.views;
 
-import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
@@ -39,14 +38,11 @@
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
-import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.util.TransformParams;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.RecentsExtraCard;
 
@@ -57,8 +53,6 @@
 public class LauncherRecentsView extends RecentsView<BaseQuickstepLauncher>
         implements StateListener<LauncherState> {
 
-    private final TransformParams mTransformParams = new TransformParams();
-
     private RecentsExtraCard mRecentsExtraCardPlugin;
     private RecentsExtraViewContainer mRecentsExtraViewContainer;
     private PluginListener<RecentsExtraCard> mRecentsExtraCardPluginListener =
@@ -108,17 +102,6 @@
         }
     }
 
-    @Override
-    public void setTranslationY(float translationY) {
-        super.setTranslationY(translationY);
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            LauncherState state = mActivity.getStateManager().getState();
-            if (state == OVERVIEW || state == ALL_APPS) {
-                redrawLiveTile(false);
-            }
-        }
-    }
-
     /**
      * Animates adjacent tasks and translate hotseat off screen as well.
      */
@@ -141,29 +124,10 @@
         }
         anim.play(ObjectAnimator.ofFloat(
                 mActivity.getAllAppsController(), ALL_APPS_PROGRESS, allAppsProgressOffscreen));
-
-        ObjectAnimator dragHandleAnim = ObjectAnimator.ofInt(
-                mActivity.getScrimView(), ScrimView.DRAG_HANDLE_ALPHA, 0);
-        dragHandleAnim.setInterpolator(Interpolators.ACCEL_2);
-        anim.play(dragHandleAnim);
-
         return anim;
     }
 
     @Override
-    protected void onTaskLaunchAnimationUpdate(float progress, TaskView tv) {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            if (tv.isRunningTask()) {
-                mTransformParams.setProgress(1 - progress)
-                        .setSyncTransactionApplier(mSyncTransactionApplier);
-                // TODO: Revisit live tiles
-            } else {
-                redrawLiveTile(true);
-            }
-        }
-    }
-
-    @Override
     protected void onTaskLaunchAnimationEnd(boolean success) {
         if (success) {
             mActivity.getStateManager().goToState(NORMAL, false /* animate */);
@@ -175,46 +139,6 @@
     }
 
     @Override
-    public void scrollTo(int x, int y) {
-        super.scrollTo(x, y);
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile) {
-            redrawLiveTile(true);
-        }
-    }
-
-    @Override
-    public TransformParams getLiveTileParams(
-            boolean mightNeedToRefill) {
-        if (!mEnableDrawingLiveTile || mRecentsAnimationController == null
-                || mRecentsAnimationTargets == null) {
-            return null;
-        }
-        TaskView taskView = getRunningTaskView();
-        if (taskView != null) {
-            taskView.getThumbnail().getGlobalVisibleRect(mTempRect);
-            int offsetX = (int) (mTaskWidth * taskView.getScaleX() * getScaleX()
-                    - mTempRect.width());
-            int offsetY = (int) (mTaskHeight * taskView.getScaleY() * getScaleY()
-                    - mTempRect.height());
-            if (((mCurrentPage != 0) || mightNeedToRefill) && offsetX > 0) {
-                if (mTempRect.left - offsetX < 0) {
-                    mTempRect.left -= offsetX;
-                } else {
-                    mTempRect.right += offsetX;
-                }
-            }
-            if (mightNeedToRefill && offsetY > 0) {
-                mTempRect.top -= offsetY;
-            }
-            mTransformParams.setProgress(1f)
-                    .setTargetAlpha(taskView.getAlpha())
-                    .setSyncTransactionApplier(mSyncTransactionApplier)
-                    .setTargetSet(mRecentsAnimationTargets);
-        }
-        return mTransformParams;
-    }
-
-    @Override
     public void reset() {
         super.reset();
 
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 a2da398..79d57c5 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
@@ -70,12 +70,14 @@
 
     @IntDef(flag = true, value = {
             DISABLED_SCROLLING,
-            DISABLED_ROTATED})
+            DISABLED_ROTATED,
+            DISABLED_NO_THUMBNAIL})
     @Retention(RetentionPolicy.SOURCE)
     public @interface ActionsDisabledFlags { }
 
     public static final int DISABLED_SCROLLING = 1 << 0;
     public static final int DISABLED_ROTATED = 1 << 1;
+    public static final int DISABLED_NO_THUMBNAIL = 1 << 2;
 
     private static final int INDEX_CONTENT_ALPHA = 0;
     private static final int INDEX_VISIBILITY_ALPHA = 1;
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 520b36e..0335ee7 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
@@ -22,7 +22,6 @@
 
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
@@ -62,6 +61,7 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
@@ -78,6 +78,7 @@
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
+import android.view.TouchDelegate;
 import android.view.View;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
@@ -129,6 +130,7 @@
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.SplitScreenBounds;
 import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.util.TransformParams;
 import com.android.systemui.plugins.ResourceProvider;
 import com.android.systemui.shared.recents.IPinnedStackAnimationListener;
@@ -209,15 +211,37 @@
                 }
             };
 
+    /** Same as normal SCALE_PROPERTY, but also updates page offsets that depend on this scale. */
+    public static final FloatProperty<RecentsView> RECENTS_SCALE_PROPERTY =
+            new FloatProperty<RecentsView>("recentsScale") {
+                @Override
+                public void setValue(RecentsView view, float scale) {
+                    view.setScaleX(scale);
+                    view.setScaleY(scale);
+                    view.mLastComputedTaskPushOutDistance = null;
+                    view.updatePageOffsets();
+                }
+
+                @Override
+                public Float get(RecentsView view) {
+                    return view.getScaleX();
+                }
+            };
+
     protected RecentsOrientedState mOrientationState;
     protected final BaseActivityInterface mSizeStrategy;
     protected RecentsAnimationController mRecentsAnimationController;
-    protected RecentsAnimationTargets mRecentsAnimationTargets;
     protected SurfaceTransactionApplier mSyncTransactionApplier;
     protected int mTaskWidth;
     protected int mTaskHeight;
+    protected final TransformParams mLiveTileParams = new TransformParams();
+    protected final TaskViewSimulator mLiveTileTaskViewSimulator;
+    protected final Rect mLastComputedTaskSize = new Rect();
+    // How much a task that is directly offscreen will be pushed out due to RecentsView scale/pivot.
+    protected Float mLastComputedTaskPushOutDistance = null;
     protected boolean mEnableDrawingLiveTile = false;
     protected final Rect mTempRect = new Rect();
+    protected final RectF mTempRectF = new RectF();
     private final PointF mTempPointF = new PointF();
 
     private static final int DISMISS_TASK_DURATION = 300;
@@ -376,7 +400,7 @@
                     mOrientationState.setMultiWindowMode(inMultiWindowMode);
                     setLayoutRotation(mOrientationState.getTouchRotation(),
                             mOrientationState.getDisplayRotation());
-                    rotateAllChildTasks();
+                    updateChildTaskOrientations();
                 }
                 if (!inMultiWindowMode && mOverviewStateEnabled) {
                     // TODO: Re-enable layout transitions for addition of the unpinned task
@@ -393,7 +417,8 @@
         mActivity = BaseActivity.fromContext(context);
         mOrientationState = new RecentsOrientedState(
                 context, mSizeStrategy, this::animateRecentsRotationInPlace);
-        mOrientationState.setActivityConfiguration(context.getResources().getConfiguration());
+        final int rotation = mActivity.getDisplay().getRotation();
+        mOrientationState.setRecentsRotation(rotation);
 
         mFastFlingVelocity = getResources()
                 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
@@ -429,6 +454,12 @@
 
         // Initialize quickstep specific cache params here, as this is constructed only once
         mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
+
+        mLiveTileTaskViewSimulator = new TaskViewSimulator(getContext(), getSizeStrategy());
+        mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
+        mLiveTileTaskViewSimulator.setLayoutRotation(getPagedViewOrientedState().getTouchRotation(),
+                getPagedViewOrientedState().getDisplayRotation());
+        mLiveTileTaskViewSimulator.setRecentsRotation(rotation);
     }
 
     public OverScroller getScroller() {
@@ -513,6 +544,7 @@
         mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
         ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
         mSyncTransactionApplier = new SurfaceTransactionApplier(this);
+        mLiveTileParams.setSyncTransactionApplier(mSyncTransactionApplier);
         RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
         mIdp.addOnChangeListener(this);
         mIPinnedStackAnimationListener.setActivity(mActivity);
@@ -530,6 +562,7 @@
         mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener);
         ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
         mSyncTransactionApplier = null;
+        mLiveTileParams.setSyncTransactionApplier(null);
         RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
         mIdp.removeOnChangeListener(this);
         SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(null);
@@ -648,6 +681,16 @@
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
         super.onTouchEvent(ev);
+
+        TaskView taskView = getCurrentPageTaskView();
+        if (taskView != null) {
+            TouchDelegate mChildTouchDelegate = taskView.getIconTouchDelegate(ev);
+            if (mChildTouchDelegate != null && mChildTouchDelegate.onTouchEvent(ev)) {
+                // Keep consuming events to pass to delegate
+                return true;
+            }
+        }
+
         final int x = (int) ev.getX();
         final int y = (int) ev.getY();
         switch (ev.getAction()) {
@@ -806,6 +849,14 @@
                 taskView.setModalness(mTaskModalness);
             }
         }
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            // Since we reuse the same mLiveTileTaskViewSimulator in the RecentsView, we need
+            // to reset the params after it settles in Overview from swipe up so that we don't
+            // render with obsolete param values.
+            mLiveTileTaskViewSimulator.fullScreenProgress.value = 0;
+            mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
+            mLiveTileTaskViewSimulator.setOffsetY(0);
+        }
         if (mRunningTaskTileHidden) {
             setRunningTaskHidden(mRunningTaskTileHidden);
         }
@@ -847,6 +898,7 @@
     public void setInsets(Rect insets) {
         mInsets.set(insets);
         resetPaddingFromTaskSize();
+        mLiveTileTaskViewSimulator.setDp(mActivity.getDeviceProfile());
     }
 
     private void resetPaddingFromTaskSize() {
@@ -864,6 +916,7 @@
     public void getTaskSize(Rect outRect) {
         mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), outRect,
                 mOrientationHandler);
+        mLastComputedTaskSize.set(outRect);
     }
 
     /** Gets the task size for modal state. */
@@ -888,6 +941,12 @@
 
         // Update the high res thumbnail loader state
         mModel.getThumbnailCache().getHighResLoadingState().setFlingingFast(isFlingingFast);
+
+        mLiveTileTaskViewSimulator.setScroll(getScrollOffset());
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
+                && mLiveTileParams.getTargetSet() != null) {
+            redrawLiveTile();
+        }
         return scrolling;
     }
 
@@ -905,8 +964,8 @@
         final int pageCount = getPageCount();
         for (int i = 0; i < pageCount; i++) {
             View page = getPageAt(i);
-            mScrollState.updateInterpolation(mOrientationHandler.getChildStartWithTranslation(page),
-                    mPageSpacing);
+            mScrollState.updateInterpolation(
+                    mOrientationHandler.getChildStartWithTranslation(page));
             ((PageCallbacks) page).onPageScroll(mScrollState);
         }
     }
@@ -995,7 +1054,7 @@
         mTaskListChangeId = -1;
 
         mRecentsAnimationController = null;
-        mRecentsAnimationTargets = null;
+        mLiveTileParams.setTargetSet(null);
 
         unloadVisibleTaskData();
         setCurrentPage(0);
@@ -1077,7 +1136,7 @@
         pa.addListener(AnimationSuccessListener.forRunnable(() -> {
             setLayoutRotation(newRotation, mOrientationState.getDisplayRotation());
             mActivity.getDragLayer().recreateControllers();
-            rotateAllChildTasks();
+            updateChildTaskOrientations();
             setRecentsChangedOrientation(false).start();
         }));
         pa.start();
@@ -1098,7 +1157,7 @@
     }
 
 
-    private void rotateAllChildTasks() {
+    private void updateChildTaskOrientations() {
         for (int i = 0; i < getTaskViewCount(); i++) {
             getTaskViewAt(i).setOrientationState(mOrientationState);
         }
@@ -1328,10 +1387,14 @@
         /**
          * Updates linearInterpolation for the provided child position
          */
-        public void updateInterpolation(float childStart, int pageSpacing) {
-            float pageCenter = childStart + halfPageSize;
+        public void updateInterpolation(float childStart) {
+            float scaledHalfPageSize = halfPageSize / pageParentScale;
+            float pageCenter = childStart + scaledHalfPageSize;
             float distanceFromScreenCenter = screenCenter - pageCenter;
-            float distanceToReachEdge = halfScreenSize + halfPageSize + pageSpacing;
+            // How far the page has to move from the center to be offscreen, taking into account
+            // the EDGE_SCALE_DOWN_FACTOR that will be applied at that position.
+            float distanceToReachEdge = halfScreenSize
+                    + scaledHalfPageSize * (1 - TaskView.EDGE_SCALE_DOWN_FACTOR);
             linearInterpolation = Math.min(1,
                     Math.abs(distanceFromScreenCenter) / distanceToReachEdge);
         }
@@ -1446,6 +1509,13 @@
             anim.addOnFrameCallback(this::updateCurveProperties);
         }
 
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && getRunningTaskView() == taskView) {
+            anim.addOnFrameCallback(() -> {
+                mLiveTileTaskViewSimulator.setOffsetY(taskView.getTranslationY());
+                redrawLiveTile();
+            });
+        }
+
         // Add a tiny bit of translation Z, so that it draws on top of other views
         if (animateTaskView) {
             taskView.setTranslationZ(0.1f);
@@ -1648,15 +1718,20 @@
         super.setVisibility(visibility);
         if (mActionsView != null) {
             mActionsView.updateHiddenFlags(HIDDEN_NO_RECENTS, visibility != VISIBLE);
+            if (visibility != VISIBLE) {
+                mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false);
+            }
         }
     }
 
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
-        if (mOrientationState.setActivityConfiguration(newConfig)) {
+        final int rotation = mActivity.getDisplay().getRotation();
+        if (mOrientationState.setRecentsRotation(rotation)) {
             updateOrientationHandler();
         }
+        mLiveTileTaskViewSimulator.setRecentsRotation(rotation);
     }
 
     public void setLayoutRotation(int touchRotation, int displayRotation) {
@@ -1680,10 +1755,13 @@
                 || mOrientationState.getRecentsActivityRotation() != ROTATION_0;
         mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION,
                 !mOrientationState.canRecentsActivityRotate() && isInLandscape);
+        updateChildTaskOrientations();
         resetPaddingFromTaskSize();
         requestLayout();
         // Reapply the current page to update page scrolls.
         setCurrentPage(mCurrentPage);
+        mLiveTileTaskViewSimulator.setLayoutRotation(getPagedViewOrientedState().getTouchRotation(),
+                getPagedViewOrientedState().getDisplayRotation());
     }
 
     public RecentsOrientedState getPagedViewOrientedState() {
@@ -1765,14 +1843,15 @@
         setPivotX(mTempPointF.x);
         setPivotY(mTempPointF.y);
         setTaskModalness(mTaskModalness);
+        mLastComputedTaskPushOutDistance = null;
         updatePageOffsets();
         setImportantForAccessibility(isModal() ? IMPORTANT_FOR_ACCESSIBILITY_NO
                 : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
     }
 
     private void updatePageOffsets() {
-        float offset = mAdjacentPageOffset * getWidth();
-        float modalOffset = ACCEL_0_75.getInterpolation(mTaskModalness) * getWidth();
+        float offset = mAdjacentPageOffset;
+        float modalOffset = ACCEL_0_75.getInterpolation(mTaskModalness);
         if (mIsRtl) {
             offset = -offset;
             modalOffset = -modalOffset;
@@ -1781,19 +1860,90 @@
 
         TaskView runningTask = mRunningTaskId == -1 || !mRunningTaskTileHidden
                 ? null : getTaskView(mRunningTaskId);
-        int midPoint = runningTask == null ? -1 : indexOfChild(runningTask);
-        int currentPage = getCurrentPage();
+        int midpoint = runningTask == null ? -1 : indexOfChild(runningTask);
+        int modalMidpoint = getCurrentPage();
+
+        float midpointOffsetSize = 0;
+        float leftOffsetSize = midpoint - 1 >= 0
+                ? -getOffsetSize(midpoint - 1, midpoint, offset)
+                : 0;
+        float rightOffsetSize = midpoint + 1 < count
+                ? getOffsetSize(midpoint + 1, midpoint, offset)
+                : 0;
+
+        float modalMidpointOffsetSize = 0;
+        float modalLeftOffsetSize = modalMidpoint - 1 >= 0
+                ? -getOffsetSize(modalMidpoint - 1, modalMidpoint, modalOffset)
+                : 0;
+        float modalRightOffsetSize = modalMidpoint + 1 < count
+                ? getOffsetSize(modalMidpoint + 1, modalMidpoint, modalOffset)
+                : 0;
 
         for (int i = 0; i < count; i++) {
-            float translation = i == midPoint ? 0 : (i < midPoint ? -offset : offset);
-            float modalTranslation =
-                    i == currentPage ? 0 : (i < currentPage ? -modalOffset : modalOffset);
+            float translation = i == midpoint
+                    ? midpointOffsetSize
+                    : i < midpoint
+                            ? leftOffsetSize
+                            : rightOffsetSize;
+            float modalTranslation = i == modalMidpoint
+                    ? modalMidpointOffsetSize
+                    : i < modalMidpoint
+                            ? modalLeftOffsetSize
+                            : modalRightOffsetSize;
             getChildAt(i).setTranslationX(translation + modalTranslation);
         }
         updateCurveProperties();
     }
 
     /**
+     * Computes the distance to offset the given child such that it is completely offscreen when
+     * translating away from the given midpoint.
+     * @param offsetProgress From 0 to 1 where 0 means no offset and 1 means offset offscreen.
+     */
+    private float getOffsetSize(int childIndex, int midpointIndex, float offsetProgress) {
+        if (offsetProgress == 0) {
+            // Don't bother calculating everything below if we won't offset anyway.
+            return 0;
+        }
+        // First, get the position of the task relative to the midpoint. If there is no midpoint
+        // then we just use the normal (centered) task position.
+        mTempRectF.set(mLastComputedTaskSize);
+        RectF taskPosition = mTempRectF;
+        float desiredLeft = getWidth();
+        float distanceToOffscreen = desiredLeft - taskPosition.left;
+        // Used to calculate the scale of the task view based on its new offset.
+        float centerToOffscreenProgress = Math.abs(offsetProgress);
+        if (midpointIndex > -1) {
+            // When there is a midpoint reference task, adjacent tasks have less distance to travel
+            // to reach offscreen. Offset the task position to the task's starting point.
+            View child = getChildAt(childIndex);
+            View midpointChild = getChildAt(midpointIndex);
+            int distanceFromMidpoint = Math.abs(mOrientationHandler.getChildStart(child)
+                    - mOrientationHandler.getChildStart(midpointChild)
+                    + getDisplacementFromScreenCenter(midpointIndex));
+            taskPosition.offset(distanceFromMidpoint, 0);
+            centerToOffscreenProgress = Utilities.mapRange(centerToOffscreenProgress,
+                    distanceFromMidpoint / distanceToOffscreen, 1);
+        }
+        // Find the task's scale based on its offscreen progress, then see how far it still needs to
+        // move to be completely offscreen.
+        Utilities.scaleRectFAboutCenter(taskPosition,
+                TaskView.getCurveScaleForInterpolation(centerToOffscreenProgress));
+        distanceToOffscreen = desiredLeft - taskPosition.left;
+        // Finally, we need to account for RecentsView scale, because it moves tasks based on its
+        // pivot. To do this, we move the task position to where it would be offscreen at scale = 1
+        // (computed above), then we apply the scale via getMatrix() to determine how much that
+        // moves the task from its desired position, and adjust the computed distance accordingly.
+        if (mLastComputedTaskPushOutDistance == null) {
+            taskPosition.offsetTo(desiredLeft, 0);
+            getMatrix().mapRect(taskPosition);
+            mLastComputedTaskPushOutDistance = (taskPosition.left - desiredLeft) / getScaleX();
+        }
+        distanceToOffscreen -= mLastComputedTaskPushOutDistance;
+        return distanceToOffscreen * offsetProgress;
+    }
+
+    /**
      * TODO: Do not assume motion across X axis for adjacent page
      */
     public float getPageOffsetScale() {
@@ -1891,7 +2041,7 @@
         float toScale = getMaxScaleForFullScreen();
         if (launchingCenterTask) {
             RecentsView recentsView = tv.getRecentsView();
-            anim.play(ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, toScale));
+            anim.play(ObjectAnimator.ofFloat(recentsView, RECENTS_SCALE_PROPERTY, toScale));
             anim.play(ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS, 1));
         } else {
             // We are launching an adjacent task, so parallax the center and other adjacent task.
@@ -1941,8 +2091,6 @@
                             ? targetSysUiFlags
                             : 0);
 
-            onTaskLaunchAnimationUpdate(animator.getAnimatedFraction(), tv);
-
             // Passing the threshold from taskview to fullscreen app will vibrate
             final boolean passed = animator.getAnimatedFraction() >=
                     SUCCESS_TRANSITION_PROGRESS;
@@ -1966,6 +2114,10 @@
 
         mPendingAnimation = new PendingAnimation(duration);
         mPendingAnimation.add(anim);
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            mLiveTileTaskViewSimulator.addOverviewToAppAnim(mPendingAnimation, interpolator);
+            mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
+        }
         mPendingAnimation.addEndListener((endState) -> {
             if (endState.isSuccess) {
                 Consumer<Boolean> onLaunchResult = (result) -> {
@@ -1991,9 +2143,6 @@
         return mPendingAnimation;
     }
 
-    protected void onTaskLaunchAnimationUpdate(float progress, TaskView tv) {
-    }
-
     protected void onTaskLaunchAnimationEnd(boolean success) {
         if (success) {
             resetTaskVisuals();
@@ -2060,13 +2209,23 @@
         mEnableDrawingLiveTile = enableDrawingLiveTile;
     }
 
-    public void redrawLiveTile(boolean mightNeedToRefill) { }
+    public void redrawLiveTile() {
+        mLiveTileTaskViewSimulator.apply(mLiveTileParams);
+    }
+
+    public TaskViewSimulator getLiveTileTaskViewSimulator() {
+        return mLiveTileTaskViewSimulator;
+    }
 
     // TODO: To be removed in a follow up CL
     public void setRecentsAnimationTargets(RecentsAnimationController recentsAnimationController,
             RecentsAnimationTargets recentsAnimationTargets) {
         mRecentsAnimationController = recentsAnimationController;
-        mRecentsAnimationTargets = recentsAnimationTargets;
+        if (recentsAnimationTargets != null) {
+            mLiveTileTaskViewSimulator.setPreview(
+                    recentsAnimationTargets.apps[recentsAnimationTargets.apps.length - 1]);
+            mLiveTileParams.setTargetSet(recentsAnimationTargets);
+        }
     }
 
     public void setLiveTileOverlayAttached(boolean liveTileOverlayAttached) {
@@ -2202,11 +2361,6 @@
         };
     }
 
-    public TransformParams getLiveTileParams(
-            boolean mightNeedToRefill) {
-        return null;
-    }
-
     private void updateEnabledOverlays() {
         int overlayEnabledPage = mOverlayEnabled ? getNextPage() : -1;
         int taskCount = getTaskViewCount();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
index ef66b7a..8b49f2c 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskMenuView.java
@@ -169,7 +169,9 @@
         }
         if (mIsOpen) {
             mOptionLayout.removeAllViews();
-            populateAndLayoutMenu();
+            if (!populateAndLayoutMenu()) {
+                close(false);
+            }
         }
     }
 
@@ -186,14 +188,22 @@
         }
         mActivity.getDragLayer().addView(this);
         mTaskView = taskView;
-        populateAndLayoutMenu();
+        if (!populateAndLayoutMenu()) {
+            return false;
+        }
         post(this::animateOpen);
         return true;
     }
 
-    private void populateAndLayoutMenu() {
+    /** @return true if successfully able to populate task view menu, false otherwise */
+    private boolean populateAndLayoutMenu() {
+        if (mTaskView.getTask().icon == null) {
+            // Icon may not be loaded
+            return false;
+        }
         addMenuOptions(mTaskView);
         orientAroundTaskView(mTaskView);
+        return true;
     }
 
     private void addMenuOptions(TaskView taskView) {
@@ -240,8 +250,10 @@
         setLayoutParams(params);
         setScaleX(taskView.getScaleX());
         setScaleY(taskView.getScaleY());
+        boolean canActivityRotate = taskView.getRecentsView()
+            .mOrientationState.canRecentsActivityRotate();
         mOptionLayout.setOrientation(orientationHandler
-                .getTaskMenuLayoutOrientation(mOptionLayout));
+                .getTaskMenuLayoutOrientation(canActivityRotate, mOptionLayout));
         setPosition(sTempRect.left - insets.left, sTempRect.top - insets.top,
             taskView.getPagedOrientationHandler());
     }
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 b2f937f..a8d6442 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
@@ -59,7 +59,6 @@
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.ConfigurationCompat;
 
 /**
  * A task in the Recents view.
@@ -357,7 +356,7 @@
     }
 
     private void updateOverlay() {
-        if (mOverlayEnabled && mBitmapShader != null && mThumbnailData != null) {
+        if (mOverlayEnabled) {
             mOverlay.initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix,
                     mPreviewPositionHelper.mIsOrientationChanged);
         } else {
@@ -385,8 +384,8 @@
         if (mBitmapShader != null && mThumbnailData != null) {
             mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(),
                     mThumbnailData.thumbnail.getHeight());
-            int currentRotation = ConfigurationCompat.getWindowConfigurationRotation(
-                    mActivity.getResources().getConfiguration());
+            int currentRotation = getTaskView().getRecentsView().getPagedViewOrientedState()
+                    .getRecentsActivityRotation();
             mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
                     getMeasuredWidth(), getMeasuredHeight(), mActivity.getDeviceProfile(),
                     currentRotation);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
index 82fabac..27d84e6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
@@ -22,10 +22,14 @@
 import static android.view.Gravity.END;
 import static android.view.Gravity.START;
 import static android.view.Gravity.TOP;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
 import static android.widget.Toast.LENGTH_SHORT;
 
 import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
 import static com.android.launcher3.Utilities.comp;
+import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
@@ -52,7 +56,9 @@
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.Log;
+import android.view.MotionEvent;
 import android.view.Surface;
+import android.view.TouchDelegate;
 import android.view.View;
 import android.view.ViewOutlineProvider;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -77,6 +83,7 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.TransformingTouchDelegate;
 import com.android.launcher3.util.ViewPool.Reusable;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TaskIconCache;
@@ -121,6 +128,13 @@
 
     public static final long SCALE_ICON_DURATION = 120;
     private static final long DIM_ANIM_DURATION = 700;
+    /**
+     * This technically can be a vanilla {@link TouchDelegate} class, however that class requires
+     * setting the touch bounds at construction, so we'd repeatedly be created many instances
+     * unnecessarily as scrolling occurs, whereas {@link TransformingTouchDelegate} allows touch
+     * delegated bounds only to be updated.
+     */
+    private TransformingTouchDelegate mIconTouchDelegate;
 
     private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
             Collections.singletonList(new Rect());
@@ -185,6 +199,7 @@
     private int mStackHeight;
     private View mContextualChipWrapper;
     private View mContextualChip;
+    private final float[] mIconCenterCoords = new float[2];
 
     public TaskView(Context context) {
         this(context, null);
@@ -245,6 +260,26 @@
         super.onFinishInflate();
         mSnapshotView = findViewById(R.id.snapshot);
         mIconView = findViewById(R.id.icon);
+        mIconTouchDelegate = new TransformingTouchDelegate(mIconView);
+    }
+
+    public TouchDelegate getIconTouchDelegate(MotionEvent event) {
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+            computeAndSetIconTouchDelegate();
+        }
+        return mIconTouchDelegate;
+    }
+
+    private void computeAndSetIconTouchDelegate() {
+        float iconHalfSize = mIconView.getWidth() / 2f;
+        mIconCenterCoords[0] = mIconCenterCoords[1] = iconHalfSize;
+        getDescendantCoordRelativeToAncestor(mIconView, mActivity.getDragLayer(), mIconCenterCoords,
+                false);
+        mIconTouchDelegate.setBounds(
+                (int) (mIconCenterCoords[0] - iconHalfSize),
+                (int) (mIconCenterCoords[1] - iconHalfSize),
+                (int) (mIconCenterCoords[0] + iconHalfSize),
+                (int) (mIconCenterCoords[1] + iconHalfSize));
     }
 
     /**
@@ -466,18 +501,18 @@
         int thumbnailPadding = (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin);
         LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
         switch (orientationHandler.getRotation()) {
-            case Surface.ROTATION_90:
+            case ROTATION_90:
                 iconParams.gravity = (isRtl ? START : END) | CENTER_VERTICAL;
                 iconParams.rightMargin = -thumbnailPadding;
                 iconParams.leftMargin = 0;
                 iconParams.topMargin = snapshotParams.topMargin / 2;
                 break;
-            case Surface.ROTATION_180:
+            case ROTATION_180:
                 iconParams.gravity = BOTTOM | CENTER_HORIZONTAL;
                 iconParams.bottomMargin = -thumbnailPadding;
                 iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
                 break;
-            case Surface.ROTATION_270:
+            case ROTATION_270:
                 iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
                 iconParams.leftMargin = -thumbnailPadding;
                 iconParams.rightMargin = 0;
diff --git a/quickstep/res/drawable/bg_circle.xml b/quickstep/res/drawable/bg_circle.xml
new file mode 100644
index 0000000..506177b
--- /dev/null
+++ b/quickstep/res/drawable/bg_circle.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid android:color="#FFFFFFFF" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/layout/gesture_tutorial_fragment.xml b/quickstep/res/layout/gesture_tutorial_fragment.xml
index 459d65f..43bf0ea 100644
--- a/quickstep/res/layout/gesture_tutorial_fragment.xml
+++ b/quickstep/res/layout/gesture_tutorial_fragment.xml
@@ -24,6 +24,14 @@
         android:layout_height="match_parent"
         android:background="@drawable/gesture_tutorial_ripple"/>
 
+    <com.android.launcher3.views.ClipIconView
+        android:id="@+id/gesture_tutorial_fake_icon_view"
+        android:layout_width="20dp"
+        android:layout_height="20dp"
+        android:background="@drawable/bg_circle"
+        android:backgroundTint="@color/gesture_tutorial_fake_task_view_color"
+        android:visibility="invisible" />
+
     <View
         android:id="@+id/gesture_tutorial_fake_task_view"
         android:layout_width="match_parent"
@@ -41,81 +49,81 @@
         android:id="@+id/gesture_tutorial_fragment_close_button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:padding="18dp"
-        android:layout_marginTop="30dp"
-        android:layout_marginStart="4dp"
         android:layout_alignParentStart="true"
         android:layout_alignParentTop="true"
-        android:background="@android:color/transparent"
+        android:layout_marginStart="4dp"
+        android:layout_marginTop="30dp"
         android:accessibilityTraversalAfter="@id/gesture_tutorial_fragment_titles_container"
+        android:background="@android:color/transparent"
         android:contentDescription="@string/gesture_tutorial_close_button_content_description"
-        android:tint="?android:attr/textColorPrimary"
-        android:src="@drawable/gesture_tutorial_close_button"/>
+        android:padding="18dp"
+        android:src="@drawable/gesture_tutorial_close_button"
+        android:tint="?android:attr/textColorPrimary"/>
 
     <LinearLayout
         android:id="@+id/gesture_tutorial_fragment_titles_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_marginTop="70dp"
         android:layout_alignParentTop="true"
+        android:layout_marginTop="70dp"
         android:focusable="true"
         android:gravity="center_horizontal"
         android:orientation="vertical">
 
         <TextView
             android:id="@+id/gesture_tutorial_fragment_title_view"
+            style="@style/TextAppearance.GestureTutorial.Title"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginStart="@dimen/gesture_tutorial_title_margin_start_end"
-            android:layout_marginEnd="@dimen/gesture_tutorial_title_margin_start_end"
-            style="@style/TextAppearance.GestureTutorial.Title"/>
+            android:layout_marginEnd="@dimen/gesture_tutorial_title_margin_start_end"/>
 
         <TextView
             android:id="@+id/gesture_tutorial_fragment_subtitle_view"
+            style="@style/TextAppearance.GestureTutorial.Subtitle"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_marginTop="10dp"
             android:layout_marginStart="@dimen/gesture_tutorial_subtitle_margin_start_end"
-            android:layout_marginEnd="@dimen/gesture_tutorial_subtitle_margin_start_end"
-            style="@style/TextAppearance.GestureTutorial.Subtitle"/>
+            android:layout_marginTop="10dp"
+            android:layout_marginEnd="@dimen/gesture_tutorial_subtitle_margin_start_end"/>
     </LinearLayout>
 
     <TextView
         android:id="@+id/gesture_tutorial_fragment_feedback_view"
+        style="@style/TextAppearance.GestureTutorial.Feedback"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginBottom="10dp"
-        android:layout_centerHorizontal="true"
         android:layout_above="@id/gesture_tutorial_fragment_action_button"
+        android:layout_centerHorizontal="true"
         android:layout_marginStart="@dimen/gesture_tutorial_feedback_margin_start_end"
         android:layout_marginEnd="@dimen/gesture_tutorial_feedback_margin_start_end"
-        style="@style/TextAppearance.GestureTutorial.Feedback"/>
+        android:layout_marginBottom="10dp"/>
 
     <!-- android:stateListAnimator="@null" removes shadow and normal on click behavior (increase
          of elevation and shadow) which is replaced by ripple effect in android:foreground -->
     <Button
         android:id="@+id/gesture_tutorial_fragment_action_button"
+        style="@style/TextAppearance.GestureTutorial.ButtonLabel"
         android:layout_width="142dp"
         android:layout_height="49dp"
-        android:layout_marginEnd="@dimen/gesture_tutorial_button_margin_start_end"
-        android:layout_marginBottom="48dp"
         android:layout_alignParentEnd="true"
         android:layout_alignParentBottom="true"
-        android:stateListAnimator="@null"
+        android:layout_marginEnd="@dimen/gesture_tutorial_button_margin_start_end"
+        android:layout_marginBottom="48dp"
         android:background="@drawable/gesture_tutorial_action_button_background"
         android:foreground="?android:attr/selectableItemBackgroundBorderless"
-        style="@style/TextAppearance.GestureTutorial.ButtonLabel"/>
+        android:stateListAnimator="@null"/>
 
     <Button
         android:id="@+id/gesture_tutorial_fragment_action_text_button"
+        style="@style/TextAppearance.GestureTutorial.TextButtonLabel"
         android:layout_width="142dp"
         android:layout_height="49dp"
-        android:layout_marginStart="@dimen/gesture_tutorial_button_margin_start_end"
-        android:layout_marginBottom="48dp"
         android:layout_alignParentStart="true"
         android:layout_alignParentBottom="true"
-        android:stateListAnimator="@null"
+        android:layout_marginStart="@dimen/gesture_tutorial_button_margin_start_end"
+        android:layout_marginBottom="48dp"
         android:background="@null"
         android:foreground="?android:attr/selectableItemBackgroundBorderless"
-        style="@style/TextAppearance.GestureTutorial.TextButtonLabel"/>
+        android:stateListAnimator="@null"/>
 </RelativeLayout>
\ No newline at end of file
diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/res/layout/overview_actions_container.xml
index e05688e..258f24a 100644
--- a/quickstep/res/layout/overview_actions_container.xml
+++ b/quickstep/res/layout/overview_actions_container.xml
@@ -17,9 +17,7 @@
 <com.android.quickstep.views.OverviewActionsView xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="@dimen/overview_actions_height"
-    android:layout_gravity="center_horizontal|bottom"
-    android:layout_marginLeft="@dimen/overview_actions_horizontal_margin"
-    android:layout_marginRight="@dimen/overview_actions_horizontal_margin">
+    android:layout_gravity="center_horizontal|bottom">
 
     <LinearLayout
         android:id="@+id/action_buttons"
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 6b941be..235df42 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -111,6 +111,13 @@
     }
 
     @Override
+    protected void handleGestureContract(Intent intent) {
+        if (FeatureFlags.SEPARATE_RECENTS_ACTIVITY.get()) {
+            super.handleGestureContract(intent);
+        }
+    }
+
+    @Override
     public void onTrimMemory(int level) {
         super.onTrimMemory(level);
         RecentsModel.INSTANCE.get(this).onTrimMemory(level);
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
index f42b124..cc7b712 100644
--- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java
+++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
@@ -43,9 +43,11 @@
 import android.util.Log;
 
 import androidx.annotation.MainThread;
+import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.popup.RemoteActionShortcut;
 import com.android.launcher3.popup.SystemShortcut;
@@ -57,6 +59,7 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.function.Consumer;
 
 /**
  * Data model for digital wellbeing status of apps.
@@ -72,6 +75,9 @@
     private static final int MSG_FULL_REFRESH = 3;
 
     // Welbeing contract
+    private static final String PATH_ACTIONS = "actions";
+    private static final String PATH_MINIMAL_DEVICE = "minimal_device";
+    private static final String METHOD_GET_MINIMAL_DEVICE_CONFIG = "get_minimal_device_config";
     private static final String METHOD_GET_ACTIONS = "get_actions";
     private static final String EXTRA_ACTIONS = "actions";
     private static final String EXTRA_ACTION = "action";
@@ -104,15 +110,22 @@
         mContentObserver = new ContentObserver(MAIN_EXECUTOR.getHandler()) {
             @Override
             public void onChange(boolean selfChange, Uri uri) {
-                // Wellbeing reports that app actions have changed.
                 if (DEBUG || mIsInTest) {
-                    Log.d(TAG, "ContentObserver.onChange() called with: selfChange = [" + selfChange
-                            + "], uri = [" + uri + "]");
+                    Log.d(TAG, "ContentObserver.onChange() called with: selfChange = ["
+                            + selfChange + "], uri = [" + uri + "]");
                 }
                 Preconditions.assertUIThread();
-                updateWellbeingData();
+
+                if (uri.getPath().contains(PATH_ACTIONS)) {
+                    // Wellbeing reports that app actions have changed.
+                    updateWellbeingData();
+                } else if (uri.getPath().contains(PATH_MINIMAL_DEVICE)) {
+                    // Wellbeing reports that minimal device state or config is changed.
+                    updateLauncherModel();
+                }
             }
         };
+        FeatureFlags.ENABLE_MINIMAL_DEVICE.addChangeListener(mContext, this::updateLauncherModel);
 
         if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
             context.registerReceiver(
@@ -146,14 +159,18 @@
     private void restartObserver() {
         final ContentResolver resolver = mContext.getContentResolver();
         resolver.unregisterContentObserver(mContentObserver);
-        Uri actionsUri = apiBuilder().path("actions").build();
+        Uri actionsUri = apiBuilder().path(PATH_ACTIONS).build();
+        Uri minimalDeviceUri = apiBuilder().path(PATH_MINIMAL_DEVICE).build();
         try {
             resolver.registerContentObserver(
                     actionsUri, true /* notifyForDescendants */, mContentObserver);
+            resolver.registerContentObserver(
+                    minimalDeviceUri, true /* notifyForDescendants */, mContentObserver);
         } catch (Exception e) {
             Log.e(TAG, "Failed to register content observer for " + actionsUri + ": " + e);
             if (mIsInTest) throw new RuntimeException(e);
         }
+
         updateWellbeingData();
     }
 
@@ -191,12 +208,42 @@
         mWorkerHandler.sendEmptyMessage(MSG_FULL_REFRESH);
     }
 
+    private void updateLauncherModel() {
+        if (!FeatureFlags.ENABLE_MINIMAL_DEVICE.get()) return;
+
+        // TODO: init Launcher in minimal device / normal mode
+    }
+
     private Uri.Builder apiBuilder() {
         return new Uri.Builder()
                 .scheme(SCHEME_CONTENT)
                 .authority(mWellbeingProviderPkg + ".api");
     }
 
+    /**
+     * Fetch most up-to-date minimal device config.
+     */
+    @WorkerThread
+    private void runWithMinimalDeviceConfigs(Consumer<Bundle> consumer) {
+        if (DEBUG || mIsInTest) {
+            Log.d(TAG, "runWithMinimalDeviceConfigs() called");
+        }
+        Preconditions.assertNonUiThread();
+
+        final Uri contentUri = apiBuilder().build();
+        final Bundle remoteBundle;
+        try (ContentProviderClient client = mContext.getContentResolver()
+                .acquireUnstableContentProviderClient(contentUri)) {
+            remoteBundle = client.call(
+                    METHOD_GET_MINIMAL_DEVICE_CONFIG, null /* args */, null /* extras */);
+            consumer.accept(remoteBundle);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to retrieve data from " + contentUri + ": " + e);
+            if (mIsInTest) throw new RuntimeException(e);
+        }
+        if (DEBUG || mIsInTest) Log.i(TAG, "runWithMinimalDeviceConfigs(): finished");
+    }
+
     private boolean updateActions(String... packageNames) {
         if (packageNames.length == 0) {
             return true;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index ec3a490..6e120e8 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3.uioverrides;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS;
@@ -29,6 +28,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
+import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 
 import android.util.FloatProperty;
 
@@ -61,7 +61,7 @@
     @Override
     public void setState(@NonNull LauncherState state) {
         float[] scaleAndOffset = state.getOverviewScaleAndOffset(mLauncher);
-        SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
+        RECENTS_SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
         ADJACENT_PAGE_OFFSET.set(mRecentsView, scaleAndOffset[1]);
 
         getContentAlphaProperty().set(mRecentsView, state.overviewUi ? 1f : 0);
@@ -93,7 +93,7 @@
     void setStateWithAnimationInternal(@NonNull final LauncherState toState,
             @NonNull StateAnimationConfig config, @NonNull PendingAnimation setter) {
         float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
-        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0],
+        setter.setFloat(mRecentsView, RECENTS_SCALE_PROPERTY, scaleAndOffset[0],
                 config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
         setter.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1],
                 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index bdeb3a6..9dc2132 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -15,12 +15,11 @@
  */
 package com.android.quickstep;
 
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.anim.Interpolators.INSTANT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
-import static com.android.quickstep.BaseSwipeUpHandlerV2.RECENTS_ATTACH_DURATION;
+import static com.android.quickstep.AbsSwipeUpHandler.RECENTS_ATTACH_DURATION;
 import static com.android.quickstep.SysUINavigationMode.getMode;
 import static com.android.quickstep.SysUINavigationMode.hideShelfInTwoButtonLandscape;
 import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
@@ -28,6 +27,7 @@
 import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_TRANSLATE_X_ANIM;
 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 android.animation.Animator;
 import android.annotation.TargetApi;
@@ -150,7 +150,7 @@
         return deviceState.isInDeferredGestureRegion(ev);
     }
 
-    public abstract void onExitOverview(RecentsAnimationDeviceState deviceState,
+    public abstract void onExitOverview(RotationTouchHelper deviceState,
             Runnable exitRunnable);
 
     /**
@@ -393,7 +393,7 @@
         protected void createBackgroundToOverviewAnim(ACTIVITY_TYPE activity, PendingAnimation pa) {
             //  Scale down recents from being full screen to being in overview.
             RecentsView recentsView = activity.getOverviewPanel();
-            pa.addFloat(recentsView, SCALE_PROPERTY,
+            pa.addFloat(recentsView, RECENTS_SCALE_PROPERTY,
                     recentsView.getMaxScaleForFullScreen(), 1, LINEAR);
             pa.addFloat(recentsView, FULLSCREEN_PROGRESS, 1, 0, LINEAR);
         }
diff --git a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
index 0710a0a..b2bce0c 100644
--- a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
@@ -374,7 +374,7 @@
         StringBuilder regions = new StringBuilder("  currentTouchableRotations=");
         for(int i = 0; i < mSwipeTouchRegions.size(); i++) {
             OrientationRectF rectF = mSwipeTouchRegions.get(mSwipeTouchRegions.keyAt(i));
-            regions.append(rectF.mRotation).append(" ");
+            regions.append(rectF).append(" ");
         }
         pw.println(regions.toString());
         pw.println("  mNavBarGesturalHeight=" + mNavBarGesturalHeight);
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 6b0c395..7168875 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -16,11 +16,9 @@
 package com.android.quickstep;
 
 import static android.content.Intent.ACTION_USER_UNLOCKED;
-import static android.view.Surface.ROTATION_0;
 
 import static com.android.launcher3.util.DefaultDisplay.CHANGE_ALL;
 import static com.android.launcher3.util.DefaultDisplay.CHANGE_FRAME_DELAY;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS;
 import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
@@ -52,25 +50,21 @@
 import android.text.TextUtils;
 import android.util.DisplayMetrics;
 import android.view.MotionEvent;
-import android.view.OrientationEventListener;
 import android.view.Surface;
 
 import androidx.annotation.BinderThread;
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.DefaultDisplay;
 import com.android.launcher3.util.SecureSettingsObserver;
 import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
 import com.android.quickstep.SysUINavigationMode.OneHandedModeChangeListener;
 import com.android.quickstep.util.NavBarPosition;
-import com.android.quickstep.util.RecentsOrientedState;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
 import com.android.systemui.shared.system.SystemGestureExclusionListenerCompat;
-import com.android.systemui.shared.system.TaskStackChangeListener;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -85,11 +79,13 @@
         DefaultDisplay.DisplayInfoChangeListener,
         OneHandedModeChangeListener {
 
+    static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode";
+
     private final Context mContext;
     private final SysUINavigationMode mSysUiNavMode;
     private final DefaultDisplay mDefaultDisplay;
     private final int mDisplayId;
-    private int mDisplayRotation;
+    private final RotationTouchHelper mRotationTouchHelper;
 
     private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();
 
@@ -115,76 +111,10 @@
         }
     };
 
-    private TaskStackChangeListener mFrozenTaskListener = new TaskStackChangeListener() {
-        @Override
-        public void onRecentTaskListFrozenChanged(boolean frozen) {
-            mTaskListFrozen = frozen;
-            if (frozen || mInOverview) {
-                return;
-            }
-            enableMultipleRegions(false);
-        }
-
-        @Override
-        public void onActivityRotation(int displayId) {
-            super.onActivityRotation(displayId);
-            // This always gets called before onDisplayInfoChanged() so we know how to process
-            // the rotation in that method. This is done to avoid having a race condition between
-            // the sensor readings and onDisplayInfoChanged() call
-            if (displayId != mDisplayId) {
-                return;
-            }
-
-            mPrioritizeDeviceRotation = true;
-            if (mInOverview) {
-                // reset, launcher must be rotating
-                mExitOverviewRunnable.run();
-            }
-        }
-    };
-
-    private Runnable mExitOverviewRunnable = new Runnable() {
-        @Override
-        public void run() {
-            mInOverview = false;
-            enableMultipleRegions(false);
-        }
-    };
-
-    private OrientationTouchTransformer mOrientationTouchTransformer;
-    /**
-     * Used to listen for when the device rotates into the orientation of the current
-     * foreground app. For example, if a user quickswitches from a portrait to a fixed landscape
-     * app and then rotates rotates the device to match that orientation, this triggers calls to
-     * sysui to adjust the navbar.
-     */
-    private OrientationEventListener mOrientationListener;
-    private int mSensorRotation = ROTATION_0;
-    /**
-     * This is the configuration of the foreground app or the app that will be in the foreground
-     * once a quickstep gesture finishes.
-     */
-    private int mCurrentAppRotation = -1;
-    /**
-     * This flag is set to true when the device physically changes orientations. When true,
-     * we will always report the current rotation of the foreground app whenever the display
-     * changes, as it would indicate the user's intention to rotate the foreground app.
-     */
-    private boolean mPrioritizeDeviceRotation = false;
-
     private Region mExclusionRegion;
     private SystemGestureExclusionListenerCompat mExclusionListener;
 
     private final List<ComponentName> mGestureBlockedActivities;
-    private Runnable mOnDestroyFrozenTaskRunnable;
-    /**
-     * Set to true when user swipes to recents. In recents, we ignore the state of the recents
-     * task list being frozen or not to allow the user to keep interacting with nav bar rotation
-     * they went into recents with as opposed to defaulting to the default display rotation.
-     * TODO: (b/156984037) For when user rotates after entering overview
-     */
-    private boolean mInOverview;
-    private boolean mTaskListFrozen;
 
     private boolean mIsUserSetupComplete;
 
@@ -194,6 +124,8 @@
         mDefaultDisplay = DefaultDisplay.INSTANCE.get(context);
         mDisplayId = mDefaultDisplay.getInfo().id;
         runOnDestroy(() -> mDefaultDisplay.removeChangeListener(this));
+        mRotationTouchHelper = new RotationTouchHelper(context);
+        runOnDestroy(mRotationTouchHelper::destroy);
 
         // Register for user unlocked if necessary
         mIsUserUnlocked = context.getSystemService(UserManager.class)
@@ -215,10 +147,6 @@
         };
         runOnDestroy(mExclusionListener::unregister);
 
-        Resources resources = mContext.getResources();
-        mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode,
-                () -> QuickStepContract.getWindowCornerRadius(resources));
-
         // Register for navigation mode changes
         onNavigationModeChanged(mSysUiNavMode.addModeChangeListener(this));
         runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(this));
@@ -239,13 +167,15 @@
             }
         }
 
-        if (SystemProperties.getBoolean("ro.support_one_handed_mode", false)) {
+        if (SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false)) {
             SecureSettingsObserver oneHandedEnabledObserver =
                     SecureSettingsObserver.newOneHandedSettingsObserver(
                             mContext, enabled -> mIsOneHandedModeEnabled = enabled);
             oneHandedEnabledObserver.register();
             oneHandedEnabledObserver.dispatchOnChange();
             runOnDestroy(oneHandedEnabledObserver::unregister);
+        } else {
+            mIsOneHandedModeEnabled = false;
         }
 
         SecureSettingsObserver swipeBottomEnabledObserver =
@@ -265,38 +195,6 @@
             userSetupObserver.register();
             runOnDestroy(userSetupObserver::unregister);
         }
-
-        mOrientationListener = new OrientationEventListener(context) {
-            @Override
-            public void onOrientationChanged(int degrees) {
-                int newRotation = RecentsOrientedState.getRotationForUserDegreesRotated(degrees,
-                        mSensorRotation);
-                if (newRotation == mSensorRotation) {
-                    return;
-                }
-
-                mSensorRotation = newRotation;
-                mPrioritizeDeviceRotation = true;
-
-                if (newRotation == mCurrentAppRotation) {
-                    // When user rotates device to the orientation of the foreground app after
-                    // quickstepping
-                    toggleSecondaryNavBarsForRotation();
-                }
-            }
-        };
-    }
-
-    private void setupOrientationSwipeHandler() {
-        ActivityManagerWrapper.getInstance().registerTaskStackListener(mFrozenTaskListener);
-        mOnDestroyFrozenTaskRunnable = () -> ActivityManagerWrapper.getInstance()
-                .unregisterTaskStackListener(mFrozenTaskListener);
-        runOnDestroy(mOnDestroyFrozenTaskRunnable);
-    }
-
-    private void destroyOrientationSwipeHandlerCallback() {
-        ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mFrozenTaskListener);
-        mOnDestroyActions.remove(mOnDestroyFrozenTaskRunnable);
     }
 
     private void runOnDestroy(Runnable action) {
@@ -343,15 +241,6 @@
         }
 
         mNavBarPosition = new NavBarPosition(newMode, mDefaultDisplay.getInfo());
-
-        mOrientationTouchTransformer.setNavigationMode(newMode, mDefaultDisplay.getInfo(),
-                mContext.getApplicationContext().getResources());
-        if (!mMode.hasGestures && newMode.hasGestures) {
-            setupOrientationSwipeHandler();
-        } else if (mMode.hasGestures && !newMode.hasGestures){
-            destroyOrientationSwipeHandlerCallback();
-        }
-
         mMode = newMode;
     }
 
@@ -362,34 +251,15 @@
             return;
         }
 
-        mDisplayRotation = info.rotation;
-
         if (!mMode.hasGestures) {
             return;
         }
         mNavBarPosition = new NavBarPosition(mMode, info);
-        updateGestureTouchRegions();
-        mOrientationTouchTransformer.createOrAddTouchRegion(info);
-        mCurrentAppRotation = mDisplayRotation;
-
-        /* Update nav bars on the following:
-         * a) if this is coming from an activity rotation OR
-         *   aa) we launch an app in the orientation that user is already in
-         * b) We're not in overview, since overview will always be portrait (w/o home rotation)
-         * c) We're actively in quickswitch mode
-         */
-        if ((mPrioritizeDeviceRotation
-                || mCurrentAppRotation == mSensorRotation) // switch to an app of orientation user is in
-                && !mInOverview
-                && mTaskListFrozen) {
-            toggleSecondaryNavBarsForRotation();
-        }
     }
 
     @Override
     public void onOneHandedModeChanged(int newGesturalHeight) {
-        mOrientationTouchTransformer.setGesturalHeight(newGesturalHeight, mDefaultDisplay.getInfo(),
-                mContext.getApplicationContext().getResources());
+        mRotationTouchHelper.setGesturalHeight(newGesturalHeight);
     }
 
     /**
@@ -504,7 +374,7 @@
      */
     public boolean canStartSystemGesture() {
         boolean canStartWithNavHidden = (mSystemUiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) == 0
-                || mTaskListFrozen;
+                || mRotationTouchHelper.isTaskListFrozen();
         return canStartWithNavHidden
                 && (mSystemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) == 0
                 && (mSystemUiStateFlags & SYSUI_STATE_QUICK_SETTINGS_EXPANDED) == 0
@@ -577,34 +447,7 @@
     }
 
     /**
-     * Updates the regions for detecting the swipe up/quickswitch and assistant gestures.
-     */
-    public void updateGestureTouchRegions() {
-        if (!mMode.hasGestures) {
-            return;
-        }
-
-        mOrientationTouchTransformer.createOrAddTouchRegion(mDefaultDisplay.getInfo());
-    }
-
-    /**
-     * @return whether the coordinates of the {@param event} is in the swipe up gesture region.
-     */
-    public boolean isInSwipeUpTouchRegion(MotionEvent event) {
-        return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY());
-    }
-
-    /**
-     * @return whether the coordinates of the {@param event} with the given {@param pointerIndex}
-     *         is in the swipe up gesture region.
-     */
-    public boolean isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex) {
-        return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(pointerIndex),
-                event.getY(pointerIndex));
-    }
-
-    /**
-     * @return whether screen pinning is enabled and active
+     * @return whether one-handed mode is enabled and active
      */
     public boolean isOneHandedModeActive() {
         return (mSystemUiStateFlags & SYSUI_STATE_ONE_HANDED_ACTIVE) != 0;
@@ -667,7 +510,7 @@
     public boolean canTriggerAssistantAction(MotionEvent ev, ActivityManager.RunningTaskInfo task) {
         return mAssistantAvailable
                 && !QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags)
-                && mOrientationTouchTransformer.touchInAssistantRegion(ev)
+                && mRotationTouchHelper.touchInAssistantRegion(ev)
                 && !isLockToAppActive()
                 && !isGestureBlockedActivity(task);
     }
@@ -679,15 +522,19 @@
      * @return whether the given motion event can trigger the one handed mode.
      */
     public boolean canTriggerOneHandedAction(MotionEvent ev) {
+        if (!SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false)) {
+            return false;
+        }
+
         if (!mIsOneHandedModeEnabled && !mIsSwipeToNotificationEnabled) {
             return false;
         }
 
         final DefaultDisplay.Info displayInfo = mDefaultDisplay.getInfo();
-        return (mOrientationTouchTransformer.touchInOneHandedModeRegion(ev)
-                && displayInfo.rotation != Surface.ROTATION_90
-                && displayInfo.rotation != Surface.ROTATION_270
-                && displayInfo.metrics.densityDpi < DisplayMetrics.DENSITY_600);
+        return (mRotationTouchHelper.touchInOneHandedModeRegion(ev)
+            && displayInfo.rotation != Surface.ROTATION_90
+            && displayInfo.rotation != Surface.ROTATION_270
+            && displayInfo.metrics.densityDpi < DisplayMetrics.DENSITY_600);
     }
 
     public boolean isOneHandedModeEnabled() {
@@ -698,96 +545,8 @@
         return mIsSwipeToNotificationEnabled;
     }
 
-    /**
-     * *May* apply a transform on the motion event if it lies in the nav bar region for another
-     * orientation that is currently being tracked as a part of quickstep
-     */
-    void setOrientationTransformIfNeeded(MotionEvent event) {
-        // negative coordinates bug b/143901881
-        if (event.getX() < 0 || event.getY() < 0) {
-            event.setLocation(Math.max(0, event.getX()), Math.max(0, event.getY()));
-        }
-        mOrientationTouchTransformer.transform(event);
-    }
-
-    private void enableMultipleRegions(boolean enable) {
-        mOrientationTouchTransformer.enableMultipleRegions(enable, mDefaultDisplay.getInfo());
-        notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getQuickStepStartingRotation());
-        if (enable && !mInOverview && !TestProtocol.sDisableSensorRotation) {
-            // Clear any previous state from sensor manager
-            mSensorRotation = mCurrentAppRotation;
-            mOrientationListener.enable();
-        } else {
-            mOrientationListener.disable();
-        }
-    }
-
-    public void onStartGesture() {
-        if (mTaskListFrozen) {
-            // Prioritize whatever nav bar user touches once in quickstep
-            // This case is specifically when user changes what nav bar they are using mid
-            // quickswitch session before tasks list is unfrozen
-            notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
-        }
-    }
-
-    void onEndTargetCalculated(GestureState.GestureEndTarget endTarget,
-            BaseActivityInterface activityInterface) {
-        if (endTarget == GestureState.GestureEndTarget.RECENTS) {
-            mInOverview = true;
-            if (!mTaskListFrozen) {
-                // If we're in landscape w/o ever quickswitching, show the navbar in landscape
-                enableMultipleRegions(true);
-            }
-            activityInterface.onExitOverview(this, mExitOverviewRunnable);
-        } else if (endTarget == GestureState.GestureEndTarget.HOME) {
-            enableMultipleRegions(false);
-        } else if (endTarget == GestureState.GestureEndTarget.NEW_TASK) {
-            if (mOrientationTouchTransformer.getQuickStepStartingRotation() == -1) {
-                // First gesture to start quickswitch
-                enableMultipleRegions(true);
-            } else {
-                notifySysuiOfCurrentRotation(
-                        mOrientationTouchTransformer.getCurrentActiveRotation());
-            }
-
-            // A new gesture is starting, reset the current device rotation
-            // This is done under the assumption that the user won't rotate the phone and then
-            // quickswitch in the old orientation.
-            mPrioritizeDeviceRotation = false;
-        } else if (endTarget == GestureState.GestureEndTarget.LAST_TASK) {
-            if (!mTaskListFrozen) {
-                // touched nav bar but didn't go anywhere and not quickswitching, do nothing
-                return;
-            }
-            notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
-        }
-    }
-
-    private void notifySysuiOfCurrentRotation(int rotation) {
-        UI_HELPER_EXECUTOR.execute(() -> SystemUiProxy.INSTANCE.get(mContext)
-                .onQuickSwitchToNewTask(rotation));
-    }
-
-    /**
-     * Disables/Enables multiple nav bars on {@link OrientationTouchTransformer} and then
-     * notifies system UI of the primary rotation the user is interacting with
-     */
-    private void toggleSecondaryNavBarsForRotation() {
-        mOrientationTouchTransformer.setSingleActiveRegion(mDefaultDisplay.getInfo());
-        notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
-    }
-
-    public int getCurrentActiveRotation() {
-        if (!mMode.hasGestures) {
-            // touch rotation should always match that of display for 3 button
-            return mDisplayRotation;
-        }
-        return mOrientationTouchTransformer.getCurrentActiveRotation();
-    }
-
-    public int getDisplayRotation() {
-        return mDisplayRotation;
+    public RotationTouchHelper getRotationTouchHelper() {
+        return mRotationTouchHelper;
     }
 
     public void dump(PrintWriter pw) {
@@ -799,11 +558,9 @@
         pw.println("  assistantAvailable=" + mAssistantAvailable);
         pw.println("  assistantDisabled="
                 + QuickStepContract.isAssistantGestureDisabled(mSystemUiStateFlags));
-        pw.println("  currentActiveRotation=" + getCurrentActiveRotation());
-        pw.println("  displayRotation=" + getDisplayRotation());
         pw.println("  isUserUnlocked=" + mIsUserUnlocked);
         pw.println("  isOneHandedModeEnabled=" + mIsOneHandedModeEnabled);
         pw.println("  isSwipeToNotificationEnabled=" + mIsSwipeToNotificationEnabled);
-        mOrientationTouchTransformer.dump(pw);
+        mRotationTouchHelper.dump(pw);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
new file mode 100644
index 0000000..2b5e42a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -0,0 +1,376 @@
+/*
+ * 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;
+
+import static android.view.Surface.ROTATION_0;
+
+import static com.android.launcher3.util.DefaultDisplay.CHANGE_ALL;
+import static com.android.launcher3.util.DefaultDisplay.CHANGE_FRAME_DELAY;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.view.MotionEvent;
+import android.view.OrientationEventListener;
+
+import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.quickstep.util.RecentsOrientedState;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.QuickStepContract;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+public class RotationTouchHelper implements
+        SysUINavigationMode.NavigationModeChangeListener,
+        DefaultDisplay.DisplayInfoChangeListener {
+
+    private final OrientationTouchTransformer mOrientationTouchTransformer;
+    private final DefaultDisplay mDefaultDisplay;
+    private final SysUINavigationMode mSysUiNavMode;
+    private final int mDisplayId;
+    private int mDisplayRotation;
+
+    private final ArrayList<Runnable> mOnDestroyActions = new ArrayList<>();
+
+    private SysUINavigationMode.Mode mMode = THREE_BUTTONS;
+
+    private TaskStackChangeListener mFrozenTaskListener = new TaskStackChangeListener() {
+        @Override
+        public void onRecentTaskListFrozenChanged(boolean frozen) {
+            mTaskListFrozen = frozen;
+            if (frozen || mInOverview) {
+                return;
+            }
+            enableMultipleRegions(false);
+        }
+
+        @Override
+        public void onActivityRotation(int displayId) {
+            super.onActivityRotation(displayId);
+            // This always gets called before onDisplayInfoChanged() so we know how to process
+            // the rotation in that method. This is done to avoid having a race condition between
+            // the sensor readings and onDisplayInfoChanged() call
+            if (displayId != mDisplayId) {
+                return;
+            }
+
+            mPrioritizeDeviceRotation = true;
+            if (mInOverview) {
+                // reset, launcher must be rotating
+                mExitOverviewRunnable.run();
+            }
+        }
+    };
+
+    private Runnable mExitOverviewRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mInOverview = false;
+            enableMultipleRegions(false);
+        }
+    };
+
+    /**
+     * Used to listen for when the device rotates into the orientation of the current foreground
+     * app. For example, if a user quickswitches from a portrait to a fixed landscape app and then
+     * rotates rotates the device to match that orientation, this triggers calls to sysui to adjust
+     * the navbar.
+     */
+    private OrientationEventListener mOrientationListener;
+    private int mSensorRotation = ROTATION_0;
+    /**
+     * This is the configuration of the foreground app or the app that will be in the foreground
+     * once a quickstep gesture finishes.
+     */
+    private int mCurrentAppRotation = -1;
+    /**
+     * This flag is set to true when the device physically changes orientations. When true, we will
+     * always report the current rotation of the foreground app whenever the display changes, as it
+     * would indicate the user's intention to rotate the foreground app.
+     */
+    private boolean mPrioritizeDeviceRotation = false;
+    private Runnable mOnDestroyFrozenTaskRunnable;
+    /**
+     * Set to true when user swipes to recents. In recents, we ignore the state of the recents
+     * task list being frozen or not to allow the user to keep interacting with nav bar rotation
+     * they went into recents with as opposed to defaulting to the default display rotation.
+     * TODO: (b/156984037) For when user rotates after entering overview
+     */
+    private boolean mInOverview;
+    private boolean mTaskListFrozen;
+
+
+    private final Context mContext;
+
+    public RotationTouchHelper(Context context) {
+        mContext = context;
+        Resources resources = mContext.getResources();
+        mSysUiNavMode = SysUINavigationMode.INSTANCE.get(context);
+        mDefaultDisplay = DefaultDisplay.INSTANCE.get(context);
+        mDisplayId = mDefaultDisplay.getInfo().id;
+
+        mOrientationTouchTransformer = new OrientationTouchTransformer(resources, mMode,
+                () -> QuickStepContract.getWindowCornerRadius(resources));
+
+        // Register for navigation mode changes
+        onNavigationModeChanged(mSysUiNavMode.addModeChangeListener(this));
+        runOnDestroy(() -> mSysUiNavMode.removeModeChangeListener(this));
+
+        mOrientationListener = new OrientationEventListener(context) {
+            @Override
+            public void onOrientationChanged(int degrees) {
+                int newRotation = RecentsOrientedState.getRotationForUserDegreesRotated(degrees,
+                        mSensorRotation);
+                if (newRotation == mSensorRotation) {
+                    return;
+                }
+
+                mSensorRotation = newRotation;
+                mPrioritizeDeviceRotation = true;
+
+                if (newRotation == mCurrentAppRotation) {
+                    // When user rotates device to the orientation of the foreground app after
+                    // quickstepping
+                    toggleSecondaryNavBarsForRotation();
+                }
+            }
+        };
+    }
+
+    private void setupOrientationSwipeHandler() {
+        ActivityManagerWrapper.getInstance().registerTaskStackListener(mFrozenTaskListener);
+        mOnDestroyFrozenTaskRunnable = () -> ActivityManagerWrapper.getInstance()
+                .unregisterTaskStackListener(mFrozenTaskListener);
+        runOnDestroy(mOnDestroyFrozenTaskRunnable);
+    }
+
+    private void destroyOrientationSwipeHandlerCallback() {
+        ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mFrozenTaskListener);
+        mOnDestroyActions.remove(mOnDestroyFrozenTaskRunnable);
+    }
+
+    private void runOnDestroy(Runnable action) {
+        mOnDestroyActions.add(action);
+    }
+
+    /**
+     * Cleans up all the registered listeners and receivers.
+     */
+    public void destroy() {
+        for (Runnable r : mOnDestroyActions) {
+            r.run();
+        }
+    }
+
+    public boolean isTaskListFrozen() {
+        return mTaskListFrozen;
+    }
+
+    public boolean touchInAssistantRegion(MotionEvent ev) {
+        return mOrientationTouchTransformer.touchInAssistantRegion(ev);
+    }
+
+    public boolean touchInOneHandedModeRegion(MotionEvent ev) {
+        return mOrientationTouchTransformer.touchInOneHandedModeRegion(ev);
+    }
+
+    /**
+     * Updates the regions for detecting the swipe up/quickswitch and assistant gestures.
+     */
+    public void updateGestureTouchRegions() {
+        if (!mMode.hasGestures) {
+            return;
+        }
+
+        mOrientationTouchTransformer.createOrAddTouchRegion(mDefaultDisplay.getInfo());
+    }
+
+    /**
+     * @return whether the coordinates of the {@param event} is in the swipe up gesture region.
+     */
+    public boolean isInSwipeUpTouchRegion(MotionEvent event) {
+        return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY());
+    }
+
+    /**
+     * @return whether the coordinates of the {@param event} with the given {@param pointerIndex}
+     *         is in the swipe up gesture region.
+     */
+    public boolean isInSwipeUpTouchRegion(MotionEvent event, int pointerIndex) {
+        return mOrientationTouchTransformer.touchInValidSwipeRegions(event.getX(pointerIndex),
+                event.getY(pointerIndex));
+    }
+
+
+    @Override
+    public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
+        mDefaultDisplay.removeChangeListener(this);
+        mDefaultDisplay.addChangeListener(this);
+        onDisplayInfoChanged(mDefaultDisplay.getInfo(), CHANGE_ALL);
+
+        mOrientationTouchTransformer.setNavigationMode(newMode, mDefaultDisplay.getInfo(),
+            mContext.getResources());
+        if (!mMode.hasGestures && newMode.hasGestures) {
+            setupOrientationSwipeHandler();
+        } else if (mMode.hasGestures && !newMode.hasGestures){
+            destroyOrientationSwipeHandlerCallback();
+        }
+
+        mMode = newMode;
+    }
+
+    public int getDisplayRotation() {
+        return mDisplayRotation;
+    }
+
+    @Override
+    public void onDisplayInfoChanged(DefaultDisplay.Info info, int flags) {
+        if (info.id != mDisplayId|| flags == CHANGE_FRAME_DELAY) {
+            // ignore displays that aren't running launcher and frame refresh rate changes
+            return;
+        }
+
+        mDisplayRotation = info.rotation;
+
+        if (!mMode.hasGestures) {
+            return;
+        }
+        updateGestureTouchRegions();
+        mOrientationTouchTransformer.createOrAddTouchRegion(info);
+        mCurrentAppRotation = mDisplayRotation;
+
+        /* Update nav bars on the following:
+         * a) if this is coming from an activity rotation OR
+         *   aa) we launch an app in the orientation that user is already in
+         * b) We're not in overview, since overview will always be portrait (w/o home rotation)
+         * c) We're actively in quickswitch mode
+         */
+        if ((mPrioritizeDeviceRotation
+                || mCurrentAppRotation == mSensorRotation) // switch to an app of orientation user is in
+                && !mInOverview
+                && mTaskListFrozen) {
+            toggleSecondaryNavBarsForRotation();
+        }
+    }
+
+    /**
+     * Sets the gestural height.
+     */
+    void setGesturalHeight(int newGesturalHeight) {
+        mOrientationTouchTransformer.setGesturalHeight(newGesturalHeight, mDefaultDisplay.getInfo(),
+            mContext.getResources());
+    }
+
+    /**
+     * *May* apply a transform on the motion event if it lies in the nav bar region for another
+     * orientation that is currently being tracked as a part of quickstep
+     */
+    void setOrientationTransformIfNeeded(MotionEvent event) {
+        // negative coordinates bug b/143901881
+        if (event.getX() < 0 || event.getY() < 0) {
+            event.setLocation(Math.max(0, event.getX()), Math.max(0, event.getY()));
+        }
+        mOrientationTouchTransformer.transform(event);
+    }
+
+    private void enableMultipleRegions(boolean enable) {
+        mOrientationTouchTransformer.enableMultipleRegions(enable, mDefaultDisplay.getInfo());
+        notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getQuickStepStartingRotation());
+        if (enable && !mInOverview && !TestProtocol.sDisableSensorRotation) {
+            // Clear any previous state from sensor manager
+            mSensorRotation = mCurrentAppRotation;
+            mOrientationListener.enable();
+        } else {
+            mOrientationListener.disable();
+        }
+    }
+
+    public void onStartGesture() {
+        if (mTaskListFrozen) {
+            // Prioritize whatever nav bar user touches once in quickstep
+            // This case is specifically when user changes what nav bar they are using mid
+            // quickswitch session before tasks list is unfrozen
+            notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
+        }
+    }
+
+    void onEndTargetCalculated(GestureState.GestureEndTarget endTarget,
+            BaseActivityInterface activityInterface) {
+        if (endTarget == GestureState.GestureEndTarget.RECENTS) {
+            mInOverview = true;
+            if (!mTaskListFrozen) {
+                // If we're in landscape w/o ever quickswitching, show the navbar in landscape
+                enableMultipleRegions(true);
+            }
+            activityInterface.onExitOverview(this, mExitOverviewRunnable);
+        } else if (endTarget == GestureState.GestureEndTarget.HOME) {
+            enableMultipleRegions(false);
+        } else if (endTarget == GestureState.GestureEndTarget.NEW_TASK) {
+            if (mOrientationTouchTransformer.getQuickStepStartingRotation() == -1) {
+                // First gesture to start quickswitch
+                enableMultipleRegions(true);
+            } else {
+                notifySysuiOfCurrentRotation(
+                        mOrientationTouchTransformer.getCurrentActiveRotation());
+            }
+
+            // A new gesture is starting, reset the current device rotation
+            // This is done under the assumption that the user won't rotate the phone and then
+            // quickswitch in the old orientation.
+            mPrioritizeDeviceRotation = false;
+        } else if (endTarget == GestureState.GestureEndTarget.LAST_TASK) {
+            if (!mTaskListFrozen) {
+                // touched nav bar but didn't go anywhere and not quickswitching, do nothing
+                return;
+            }
+            notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
+        }
+    }
+
+    private void notifySysuiOfCurrentRotation(int rotation) {
+        UI_HELPER_EXECUTOR.execute(() -> SystemUiProxy.INSTANCE.get(mContext)
+                .onQuickSwitchToNewTask(rotation));
+    }
+
+    /**
+     * Disables/Enables multiple nav bars on {@link OrientationTouchTransformer} and then
+     * notifies system UI of the primary rotation the user is interacting with
+     */
+    private void toggleSecondaryNavBarsForRotation() {
+        mOrientationTouchTransformer.setSingleActiveRegion(mDefaultDisplay.getInfo());
+        notifySysuiOfCurrentRotation(mOrientationTouchTransformer.getCurrentActiveRotation());
+    }
+
+    public int getCurrentActiveRotation() {
+        if (!mMode.hasGestures) {
+            // touch rotation should always match that of display for 3 button
+            return mDisplayRotation;
+        }
+        return mOrientationTouchTransformer.getCurrentActiveRotation();
+    }
+
+    public void dump(PrintWriter pw) {
+        pw.println("RotationTouchHelper:");
+        pw.println("  currentActiveRotation=" + getCurrentActiveRotation());
+        pw.println("  displayRotation=" + getDisplayRotation());
+        mOrientationTouchTransformer.dump(pw);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/SysUINavigationMode.java b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
index 6994d66..6b50218 100644
--- a/quickstep/src/com/android/quickstep/SysUINavigationMode.java
+++ b/quickstep/src/com/android/quickstep/SysUINavigationMode.java
@@ -17,6 +17,9 @@
 package com.android.quickstep;
 
 import static com.android.launcher3.ResourceUtils.INVALID_RESOURCE_HANDLE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_2_BUTTON;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_3_BUTTON;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON;
 import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
 
 import android.content.BroadcastReceiver;
@@ -25,6 +28,7 @@
 import android.util.Log;
 
 import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.MainThreadInitializedObject;
 
@@ -38,16 +42,18 @@
 public class SysUINavigationMode {
 
     public enum Mode {
-        THREE_BUTTONS(false, 0),
-        TWO_BUTTONS(true, 1),
-        NO_BUTTON(true, 2);
+        THREE_BUTTONS(false, 0, LAUNCHER_NAVIGATION_MODE_3_BUTTON),
+        TWO_BUTTONS(true, 1, LAUNCHER_NAVIGATION_MODE_2_BUTTON),
+        NO_BUTTON(true, 2, LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON);
 
         public final boolean hasGestures;
         public final int resValue;
+        public final LauncherEvent launcherEvent;
 
-        Mode(boolean hasGestures, int resValue) {
+        Mode(boolean hasGestures, int resValue, LauncherEvent launcherEvent) {
             this.hasGestures = hasGestures;
             this.resValue = resValue;
+            this.launcherEvent = launcherEvent;
         }
     }
 
@@ -183,12 +189,10 @@
     }
 
     public interface NavigationModeChangeListener {
-
         void onNavigationModeChanged(Mode newMode);
     }
 
     public interface OneHandedModeChangeListener {
-
         void onOneHandedModeChanged(int newGesturalHeight);
     }
 }
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 5b239a4..e4b05ae 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -347,6 +347,19 @@
     }
 
     @Override
+    public void handleImageBundleAsScreenshot(Bundle screenImageBundle, Rect locationInScreen,
+            Insets visibleInsets, Task.TaskKey task) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.handleImageBundleAsScreenshot(screenImageBundle, locationInScreen,
+                    visibleInsets, task);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call handleImageBundleAsScreenshot");
+            }
+        }
+    }
+
+    @Override
     public void startOneHandedMode() {
         if (mSystemUiProxy != null) {
             try {
@@ -369,19 +382,6 @@
     }
 
     @Override
-    public void handleImageBundleAsScreenshot(Bundle screenImageBundle, Rect locationInScreen,
-            Insets visibleInsets, Task.TaskKey task) {
-        if (mSystemUiProxy != null) {
-            try {
-                mSystemUiProxy.handleImageBundleAsScreenshot(screenImageBundle, locationInScreen,
-                        visibleInsets, task);
-            } catch (RemoteException e) {
-                Log.w(TAG, "Failed call handleImageBundleAsScreenshot");
-            }
-        }
-    }
-
-    @Override
     public void expandNotificationPanel() {
         if (mSystemUiProxy != null) {
             try {
diff --git a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
index 0e2312b..81c4d0c 100644
--- a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
+++ b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
@@ -16,6 +16,7 @@
 package com.android.quickstep.interaction;
 
 import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_COMPLETED;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_BAD_ANGLE;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT;
@@ -48,6 +49,7 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.util.NavBarPosition;
 import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -74,6 +76,7 @@
     private final PointF mAssistantStartDragPos = new PointF();
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
+    private final MotionPauseDetector mMotionPauseDetector;
     private boolean mTouchCameFromAssistantCorner;
     private boolean mTouchCameFromNavBar;
     private boolean mPassedAssistantSlop;
@@ -100,6 +103,7 @@
                 new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/,
                         new NavBarPosition(Mode.NO_BUTTON, displayRotation),
                         null /*onInterceptTouch*/, this);
+        mMotionPauseDetector = new MotionPauseDetector(context);
 
         final Resources resources = context.getResources();
         mBottomGestureHeight =
@@ -177,12 +181,14 @@
                 }
                 mLaunchedAssistant = false;
                 mSwipeUpTouchTracker.init();
+                mMotionPauseDetector.clear();
+                mMotionPauseDetector.setOnMotionPauseListener(this::onMotionPauseChanged);
                 break;
             case MotionEvent.ACTION_MOVE:
+                mLastPos.set(event.getX(), event.getY());
                 if (!mAssistantGestureActive) {
                     break;
                 }
-                mLastPos.set(event.getX(), event.getY());
 
                 if (!mPassedAssistantSlop) {
                     // Normal gesture, ensure we pass the slop before we start tracking the gesture
@@ -213,6 +219,8 @@
                 break;
             case MotionEvent.ACTION_UP:
             case MotionEvent.ACTION_CANCEL:
+                mMotionPauseDetector.clear();
+                mMotionPauseDetector.setOnMotionPauseListener(null);
                 if (mGestureCallback != null && !intercepted && mTouchCameFromNavBar) {
                     mGestureCallback.onNavBarGestureAttempted(
                             HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION, new PointF());
@@ -239,9 +247,17 @@
         }
         mSwipeUpTouchTracker.onMotionEvent(event);
         mAssistantGestureDetector.onTouchEvent(event);
+        mMotionPauseDetector.addPosition(event);
+        mMotionPauseDetector.setDisallowPause(mLastPos.y >= mDisplaySize.y - mBottomGestureHeight);
         return intercepted;
     }
 
+    protected void onMotionPauseChanged(boolean isPaused) {
+        if (isPaused) {
+            VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
+        }
+    }
+
     /**
      * Determine if angle is larger than threshold for assistant detection
      */
diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
index 14e00dc..044e010 100644
--- a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
@@ -17,7 +17,8 @@
 
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
-import static com.android.quickstep.BaseSwipeUpHandlerV2.MAX_SWIPE_DURATION;
+import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
+import static com.android.quickstep.AbsSwipeUpHandler.MAX_SWIPE_DURATION;
 import static com.android.quickstep.interaction.TutorialController.TutorialType.HOME_NAVIGATION_COMPLETE;
 import static com.android.quickstep.interaction.TutorialController.TutorialType.OVERVIEW_NAVIGATION_COMPLETE;
 
@@ -110,6 +111,7 @@
         AnimatorListenerAdapter resetTaskView = new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation, boolean isReverse) {
+                mFakeIconView.setVisibility(View.INVISIBLE);
                 mFakeTaskView.setVisibility(View.INVISIBLE);
                 mFakeTaskView.setAlpha(1);
                 mRunningWindowAnim = null;
@@ -131,6 +133,7 @@
             });
         } else {
             anim.setViewAlpha(mFakeTaskView, 0, ACCEL);
+            anim.setViewAlpha(mFakeIconView, 0, ACCEL);
             anim.addListener(resetTaskView);
         }
         if (onEndRunnable != null) {
@@ -202,7 +205,7 @@
             // derivative of the scroll interpolator at zero, ie. 2.
             long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y));
             long duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
-            HomeAnimationFactory homeAnimFactory = new HomeAnimationFactory(null) {
+            HomeAnimationFactory homeAnimFactory = new HomeAnimationFactory() {
                 @Override
                 public AnimatorPlaybackController createActivityAnimationToHome() {
                     return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
@@ -218,6 +221,24 @@
                             fakeHomeIconLeft + fakeHomeIconSizePx,
                             fakeHomeIconTop + fakeHomeIconSizePx);
                 }
+
+                @Override
+                public void update(RectF rect, float progress, float radius) {
+                    mFakeIconView.setVisibility(View.VISIBLE);
+                    mFakeIconView.update(rect, progress,
+                            1f - SHAPE_PROGRESS_DURATION /* shapeProgressStart */,
+                            radius,
+                            false, /* isOpening */
+                            mFakeIconView, mDp,
+                            false /* isVerticalBarLayout */);
+                    mFakeIconView.setAlpha(1);
+                    mFakeTaskView.setAlpha(getWindowAlpha(progress));
+                }
+
+                @Override
+                public void onCancel() {
+                    mFakeIconView.setVisibility(View.INVISIBLE);
+                }
             };
             RectFSpringAnim windowAnim = createWindowAnimationToHome(startShift, homeAnimFactory);
             windowAnim.start(mContext, velocityPxPerMs);
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index c1918c2..73f1f8c 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -28,6 +28,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.R;
+import com.android.launcher3.views.ClipIconView;
 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback;
 
@@ -46,6 +47,7 @@
     final TextView mTitleTextView;
     final TextView mSubtitleTextView;
     final TextView mFeedbackView;
+    final ClipIconView mFakeIconView;
     final View mFakeTaskView;
     final View mRippleView;
     final RippleDrawable mRippleDrawable;
@@ -66,6 +68,7 @@
         mTitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_title_view);
         mSubtitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_subtitle_view);
         mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view);
+        mFakeIconView = rootView.findViewById(R.id.gesture_tutorial_fake_icon_view);
         mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view);
         mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view);
         mRippleDrawable = (RippleDrawable) mRippleView.getBackground();
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index ecbe734..059d158 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -16,6 +16,10 @@
 
 package com.android.quickstep.logging;
 
+import static android.text.format.DateUtils.DAY_IN_MILLIS;
+import static android.text.format.DateUtils.formatElapsedTime;
+
+import static com.android.launcher3.Utilities.getDevicePrefs;
 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.FOLDER;
 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.SEARCH_RESULT_CONTAINER;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORKSPACE_SNAPSHOT;
@@ -24,6 +28,8 @@
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__HOME;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__OVERVIEW;
 
+import static java.lang.System.currentTimeMillis;
+
 import android.content.Context;
 import android.util.Log;
 
@@ -33,6 +39,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
+import com.android.launcher3.logger.LauncherAtom.FolderContainer.ParentContainerCase;
 import com.android.launcher3.logger.LauncherAtom.FolderIcon;
 import com.android.launcher3.logger.LauncherAtom.FromState;
 import com.android.launcher3.logger.LauncherAtom.ToState;
@@ -52,6 +59,7 @@
 import com.android.systemui.shared.system.SysUiStatsLog;
 
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Optional;
 import java.util.OptionalInt;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -60,15 +68,17 @@
  * This class calls StatsLog compile time generated methods.
  *
  * To see if the logs are properly sent to statsd, execute following command.
+ * <ul>
  * $ wwdebug (to turn on the logcat printout)
  * $ wwlogcat (see logcat with grep filter on)
  * $ statsd_testdrive (see how ww is writing the proto to statsd buffer)
+ * </ul>
  */
 public class StatsLogCompatManager extends StatsLogManager {
 
     private static final String TAG = "StatsLog";
     private static final boolean IS_VERBOSE = Utilities.isPropertyEnabled(LogConfig.STATSLOG);
-
+    private static final String LAST_SNAPSHOT_TIME_MILLIS = "LAST_SNAPSHOT_TIME_MILLIS";
     private static final InstanceId DEFAULT_INSTANCE_ID = InstanceId.fakeInstanceId(0);
     // LauncherAtom.ItemInfo.getDefaultInstance() should be used but until launcher proto migrates
     // from nano to lite, bake constant to prevent robo test failure.
@@ -91,23 +101,40 @@
     }
 
     /**
-     * Logs the workspace layout information on the model thread.
+     * Logs impression of the current workspace with additional launcher events.
      */
     @Override
-    public void logSnapshot() {
+    public void logSnapshot(List<EventEnum> extraEvents) {
         LauncherAppState.getInstance(mContext).getModel().enqueueModelUpdateTask(
-                new SnapshotWorker());
+                new SnapshotWorker(extraEvents));
     }
 
     private class SnapshotWorker extends BaseModelUpdateTask {
         private final InstanceId mInstanceId;
-        SnapshotWorker() {
-            mInstanceId = new InstanceIdSequence(
-                    1 << 20 /*InstanceId.INSTANCE_ID_MAX*/).newInstanceId();
+        private final List<EventEnum> mExtraEvents;
+
+        SnapshotWorker(List<EventEnum> extraEvents) {
+            mInstanceId = new InstanceIdSequence(1 << 20 /*InstanceId.INSTANCE_ID_MAX*/)
+                    .newInstanceId();
+            this.mExtraEvents = extraEvents;
         }
 
         @Override
         public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+            long lastSnapshotTimeMillis = getDevicePrefs(mContext)
+                    .getLong(LAST_SNAPSHOT_TIME_MILLIS, 0);
+            // Log snapshot only if previous snapshot was older than a day
+            if (currentTimeMillis() - lastSnapshotTimeMillis < DAY_IN_MILLIS) {
+                if (IS_VERBOSE) {
+                    String elapsedTime = formatElapsedTime(
+                            (currentTimeMillis() - lastSnapshotTimeMillis) / 1000);
+                    Log.d(TAG, String.format(
+                            "Skipped snapshot logging since previous snapshot was %s old.",
+                            elapsedTime));
+                }
+                return;
+            }
+
             IntSparseArrayMap<FolderInfo> folders = dataModel.folders.clone();
             ArrayList<ItemInfo> workspaceItems = (ArrayList) dataModel.workspaceItems.clone();
             ArrayList<LauncherAppWidgetInfo> appWidgets = (ArrayList) dataModel.appWidgets.clone();
@@ -123,16 +150,22 @@
                         LauncherAtom.ItemInfo atomInfo = info.buildProto(fInfo);
                         writeSnapshot(atomInfo, mInstanceId);
                     }
-                } catch (Exception e) { }
+                } catch (Exception e) {
+                }
             }
             for (ItemInfo info : appWidgets) {
                 LauncherAtom.ItemInfo atomInfo = info.buildProto(null);
                 writeSnapshot(atomInfo, mInstanceId);
             }
+            mExtraEvents
+                    .forEach(eventName -> logger().withInstanceId(mInstanceId).log(eventName));
+
+            getDevicePrefs(mContext).edit()
+                    .putLong(LAST_SNAPSHOT_TIME_MILLIS, currentTimeMillis()).apply();
         }
     }
 
-    private static void writeSnapshot(LauncherAtom.ItemInfo info, InstanceId instanceId) {
+    private void writeSnapshot(LauncherAtom.ItemInfo info, InstanceId instanceId) {
         if (IS_VERBOSE) {
             Log.d(TAG, String.format("\nwriteSnapshot(%d):\n%s", instanceId.getId(), info));
         }
@@ -335,7 +368,7 @@
     }
 
     private static int getCardinality(LauncherAtom.ItemInfo info) {
-        switch (info.getContainerInfo().getContainerCase()){
+        switch (info.getContainerInfo().getContainerCase()) {
             case PREDICTED_HOTSEAT_CONTAINER:
                 return info.getContainerInfo().getPredictedHotseatContainer().getCardinality();
             case SEARCH_RESULT_CONTAINER:
@@ -400,9 +433,16 @@
     }
 
     private static int getPageId(LauncherAtom.ItemInfo info) {
+        if (info.hasTask()) {
+            return info.getTask().getIndex();
+        }
         switch (info.getContainerInfo().getContainerCase()) {
             case FOLDER:
                 return info.getContainerInfo().getFolder().getPageIndex();
+            case HOTSEAT:
+                return info.getContainerInfo().getHotseat().getIndex();
+            case PREDICTED_HOTSEAT_CONTAINER:
+                return info.getContainerInfo().getPredictedHotseatContainer().getIndex();
             default:
                 return info.getContainerInfo().getWorkspace().getPageIndex();
         }
@@ -411,6 +451,10 @@
     private static int getParentPageId(LauncherAtom.ItemInfo info) {
         switch (info.getContainerInfo().getContainerCase()) {
             case FOLDER:
+                if (info.getContainerInfo().getFolder().getParentContainerCase()
+                        == ParentContainerCase.HOTSEAT) {
+                    return info.getContainerInfo().getFolder().getHotseat().getIndex();
+                }
                 return info.getContainerInfo().getFolder().getWorkspace().getPageIndex();
             case SEARCH_RESULT_CONTAINER:
                 return info.getContainerInfo().getSearchResultContainer().getWorkspace()
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index a5d4568..969fa50 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -358,18 +358,23 @@
 
             if (count < 3) {
                 // Too few samples
-                if (count == 2) {
-                    int endPos = pointPos - 1;
-                    if (endPos < 0) {
-                        endPos += HISTORY_SIZE;
+                switch (count) {
+                    case 2: {
+                        int endPos = pointPos - 1;
+                        if (endPos < 0) {
+                            endPos += HISTORY_SIZE;
+                        }
+                        float denominator = eventTime - mHistoricTimes[endPos];
+                        if (denominator != 0) {
+                            return (mHistoricPos[pointPos] - mHistoricPos[endPos]) / denominator;
+                        }
                     }
-                    float denominator = eventTime - mHistoricTimes[endPos];
-                    if (denominator != 0) {
-                        return (eventTime - mHistoricPos[endPos]) / denominator;
-
-                    }
+                    // fall through
+                    case 1:
+                        return 0f;
+                    default:
+                        return null;
                 }
-                return null;
             }
 
             float Sxx = sxi2 - sxi * sxi / count;
diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
index 9ed2bbe..c2e67c1 100644
--- a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
+++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
@@ -87,20 +87,6 @@
             });
         }
 
-        if (!hasReachedMaxCount(ALL_APPS_COUNT)) {
-            stateManager.addStateListener(new StateListener<LauncherState>() {
-                @Override
-                public void onStateTransitionComplete(LauncherState finalState) {
-                    if (finalState == ALL_APPS) {
-                        if (incrementEventCount(ALL_APPS_COUNT)) {
-                            stateManager.removeStateListener(this);
-                            mLauncher.getScrimView().updateDragHandleVisibility();
-                        }
-                    }
-                }
-            });
-        }
-
         if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get() && !hasReachedMaxCount(
                 HOTSEAT_DISCOVERY_TIP_COUNT)) {
             stateManager.addStateListener(new StateListener<LauncherState>() {
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index d822b6c..81d24d7 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -33,7 +33,6 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
-import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.graphics.Matrix;
@@ -48,7 +47,6 @@
 
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
@@ -58,7 +56,6 @@
 import com.android.launcher3.util.WindowBounds;
 import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.SysUINavigationMode;
-import com.android.systemui.shared.system.ConfigurationCompat;
 
 import java.lang.annotation.Retention;
 import java.util.function.IntConsumer;
@@ -91,6 +88,7 @@
     private @SurfaceRotation int mTouchRotation = ROTATION_0;
     private @SurfaceRotation int mDisplayRotation = ROTATION_0;
     private @SurfaceRotation int mRecentsActivityRotation = ROTATION_0;
+    private @SurfaceRotation int mRecentsRotation = ROTATION_0 - 1;
 
     // Launcher activity supports multiple orientation, but fallback activity does not
     private static final int FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_ACTIVITY = 1 << 0;
@@ -133,8 +131,6 @@
     private int mFlags;
     private int mPreviousRotation = ROTATION_0;
 
-    @Nullable private Configuration mActivityConfiguration;
-
     /**
      * @param rotationChangeListener Callback for receiving rotation events when rotation watcher
      *                              is enabled
@@ -170,11 +166,11 @@
     }
 
     /**
-     * Sets the configuration for the recents activity, which could affect the activity's rotation
+     * Sets the rotation for the recents activity, which could affect the appearance of task view.
      * @see #update(int, int)
      */
-    public boolean setActivityConfiguration(Configuration activityConfiguration) {
-        mActivityConfiguration = activityConfiguration;
+    public boolean setRecentsRotation(@SurfaceRotation int recentsRotation) {
+        mRecentsRotation = recentsRotation;
         return update(mTouchRotation, mDisplayRotation);
     }
 
@@ -231,9 +227,7 @@
     @SurfaceRotation
     private int inferRecentsActivityRotation(@SurfaceRotation int displayRotation) {
         if (isRecentsActivityRotationAllowed()) {
-            return mActivityConfiguration == null
-                    ? displayRotation
-                    : ConfigurationCompat.getWindowConfigurationRotation(mActivityConfiguration);
+            return mRecentsRotation < ROTATION_0 ? displayRotation : mRecentsRotation;
         } else {
             return ROTATION_0;
         }
diff --git a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
index 19e278b..f1ac6a5 100644
--- a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
+++ b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
@@ -44,7 +44,6 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.uioverrides.states.OverviewState;
-import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.SysUINavigationMode;
@@ -77,15 +76,11 @@
     private final float mRadius;
     private final int mMaxScrimAlpha;
     private final Paint mPaint;
-    private final OnboardingPrefs mOnboardingPrefs;
 
     // Mid point where the alpha changes
     private int mMidAlpha;
     private float mMidProgress;
 
-    // The progress at which the drag handle starts moving up with the shelf.
-    private float mDragHandleProgress;
-
     private Interpolator mBeforeMidProgressColorInterpolator = ACCEL;
     private Interpolator mAfterMidProgressColorInterpolator = ACCEL;
 
@@ -103,7 +98,6 @@
     private boolean mRemainingScreenPathValid = false;
 
     private Mode mSysUINavigationMode;
-    private boolean mIsTwoZoneSwipeModel;
 
     public ShelfScrimView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -112,7 +106,6 @@
         mEndAlpha = Color.alpha(mEndScrim);
         mRadius = BOTTOM_CORNER_RADIUS_RATIO * Themes.getDialogCornerRadius(context);
         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        mOnboardingPrefs = mLauncher.getOnboardingPrefs();
 
         // Just assume the easiest UI for now, until we have the proper layout information.
         mDrawingFlatColor = true;
@@ -145,11 +138,9 @@
             // Show the shelf more quickly before reaching overview progress.
             mBeforeMidProgressColorInterpolator = ACCEL_2;
             mAfterMidProgressColorInterpolator = ACCEL;
-            mIsTwoZoneSwipeModel = FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get();
         } else {
             mBeforeMidProgressColorInterpolator = ACCEL;
             mAfterMidProgressColorInterpolator = Interpolators.clampToProgress(ACCEL, 0.5f, 1f);
-            mIsTwoZoneSwipeModel = false;
         }
     }
 
@@ -164,7 +155,6 @@
 
             Context context = getContext();
             if ((OVERVIEW.getVisibleElements(mLauncher) & ALL_APPS_HEADER_EXTRA) == 0) {
-                mDragHandleProgress = 1;
                 if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()
                         && SysUINavigationMode.removeShelfFromOverview(context)) {
                     // Fade in all apps background quickly to distinguish from swiping from nav bar.
@@ -182,29 +172,22 @@
                         + hotseatPadding.bottom + hotseatPadding.top;
                 float dragHandleTop =
                         Math.min(hotseatSize, LayoutUtils.getDefaultSwipeHeight(context, dp));
-                mDragHandleProgress =  1 - (dragHandleTop / mShiftRange);
             }
-            mTopOffset = dp.getInsets().top - mDragHandleSize.y;
+            mTopOffset = dp.getInsets().top;
             mShelfTopAtThreshold = mShiftRange * SCRIM_CATCHUP_THRESHOLD + mTopOffset;
         }
         updateColors();
         updateSysUiColors();
-        updateDragHandleAlpha();
         invalidate();
     }
 
     @Override
     public void updateColors() {
         super.updateColors();
-        mDragHandleOffset = 0;
         if (mDrawingFlatColor) {
             return;
         }
 
-        if (mProgress < mDragHandleProgress) {
-            mDragHandleOffset = mShiftRange * (mDragHandleProgress - mProgress);
-        }
-
         if (mProgress >= SCRIM_CATCHUP_THRESHOLD) {
             mShelfTop = mShiftRange * mProgress + mTopOffset;
         } else {
@@ -259,19 +242,7 @@
     }
 
     @Override
-    protected boolean shouldDragHandleBeVisible() {
-        boolean needsAllAppsEdu = mIsTwoZoneSwipeModel
-                && !mOnboardingPrefs.hasReachedMaxCount(OnboardingPrefs.ALL_APPS_COUNT);
-        return needsAllAppsEdu || super.shouldDragHandleBeVisible();
-    }
-
-    @Override
     protected void onDraw(Canvas canvas) {
-        drawBackground(canvas);
-        drawDragHandle(canvas);
-    }
-
-    private void drawBackground(Canvas canvas) {
         if (mDrawingFlatColor) {
             if (mCurrentFlatColor != 0) {
                 canvas.drawColor(mCurrentFlatColor);
@@ -311,9 +282,4 @@
         mPaint.setColor(mShelfColor);
         canvas.drawRoundRect(0, mShelfTop, width, height + mRadius, mRadius, mRadius, mPaint);
     }
-
-    @Override
-    public float getVisualTop() {
-        return mShelfTop;
-    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java b/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
deleted file mode 100644
index 5904fcd..0000000
--- a/quickstep/tests/src/com/android/quickstep/AppPredictionsUITests.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/**
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.quickstep;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-
-import android.app.prediction.AppPredictor;
-import android.app.prediction.AppTarget;
-import android.app.prediction.AppTargetId;
-import android.content.ComponentName;
-import android.content.pm.LauncherActivityInfo;
-import android.content.pm.LauncherApps;
-import android.os.Process;
-import android.view.View;
-
-import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.appprediction.PredictionRowView;
-import com.android.launcher3.appprediction.PredictionUiStateManager;
-import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
-import com.android.launcher3.model.AppLaunchTracker;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class AppPredictionsUITests extends AbstractQuickStepTest {
-
-    private LauncherActivityInfo mSampleApp1;
-    private LauncherActivityInfo mSampleApp2;
-    private LauncherActivityInfo mSampleApp3;
-
-    private AppPredictor.Callback mCallback;
-
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-
-        List<LauncherActivityInfo> activities = mTargetContext.getSystemService(LauncherApps.class)
-                .getActivityList(null, Process.myUserHandle());
-        mSampleApp1 = activities.get(0);
-        mSampleApp2 = activities.get(1);
-        mSampleApp3 = activities.get(2);
-
-        // Disable app tracker
-        AppLaunchTracker.INSTANCE.initializeForTesting(new AppLaunchTracker());
-        PredictionUiStateManager.INSTANCE.initializeForTesting(null);
-
-        mCallback = PredictionUiStateManager.INSTANCE.get(mTargetContext).appPredictorCallback(
-                Client.HOME);
-
-        mDevice.setOrientationNatural();
-    }
-
-    @After
-    public void tearDown() throws Throwable {
-        AppLaunchTracker.INSTANCE.initializeForTesting(null);
-        PredictionUiStateManager.INSTANCE.initializeForTesting(null);
-        mDevice.unfreezeRotation();
-    }
-
-    /**
-     * Test that prediction UI is updated as soon as we get predictions from the system
-     */
-    @Test
-    public void testPredictionExistsInAllApps() {
-        mLauncher.pressHome().switchToAllApps();
-
-        // Dispatch an update
-        sendPredictionUpdate(mSampleApp1, mSampleApp2);
-        // The first update should apply immediately.
-        waitForLauncherCondition("Predictions were not updated in loading state",
-                launcher -> getPredictedApp(launcher).size() == 2);
-    }
-
-    /**
-     * Test that prediction update is deferred if it is already visible
-     */
-    @Test
-    public void testPredictionsDeferredUntilHome() {
-        mDevice.pressHome();
-        sendPredictionUpdate(mSampleApp1, mSampleApp2);
-        mLauncher.pressHome().switchToAllApps();
-        waitForLauncherCondition("Predictions were not updated in loading state",
-                launcher -> getPredictedApp(launcher).size() == 2);
-
-        // Update predictions while all-apps is visible
-        sendPredictionUpdate(mSampleApp1, mSampleApp2, mSampleApp3);
-        assertEquals(2, getFromLauncher(this::getPredictedApp).size());
-
-        // Go home and go back to all-apps
-        mLauncher.pressHome().switchToAllApps();
-        assertEquals(3, getFromLauncher(this::getPredictedApp).size());
-    }
-
-    @Test
-    public void testPredictionsDisabled() {
-        mDevice.pressHome();
-        sendPredictionUpdate();
-        mLauncher.pressHome().switchToAllApps();
-
-        waitForLauncherCondition("Predictions were not updated in loading state",
-                launcher -> launcher.getAppsView().getFloatingHeaderView()
-                        .findFixedRowByType(PredictionRowView.class).getVisibility() == View.GONE);
-        assertFalse(PredictionUiStateManager.INSTANCE.get(mTargetContext)
-                .getCurrentState().isEnabled);
-    }
-
-    public ArrayList<BubbleTextView> getPredictedApp(Launcher launcher) {
-        PredictionRowView container = launcher.getAppsView().getFloatingHeaderView()
-                .findFixedRowByType(PredictionRowView.class);
-
-        ArrayList<BubbleTextView> predictedAppViews = new ArrayList<>();
-        for (int i = 0; i < container.getChildCount(); i++) {
-            View view = container.getChildAt(i);
-            if (view instanceof BubbleTextView && view.getVisibility() == View.VISIBLE) {
-                predictedAppViews.add((BubbleTextView) view);
-            }
-        }
-        return predictedAppViews;
-    }
-
-    private void sendPredictionUpdate(LauncherActivityInfo... activities) {
-        getOnUiThread(() -> {
-            List<AppTarget> targets = new ArrayList<>(activities.length);
-            for (LauncherActivityInfo info : activities) {
-                ComponentName cn = info.getComponentName();
-                AppTarget target = new AppTarget.Builder(
-                        new AppTargetId("app:" + cn), cn.getPackageName(), info.getUser())
-                        .setClassName(cn.getClassName())
-                        .build();
-                targets.add(target);
-            }
-            mCallback.onTargetsAvailable(targets);
-            return null;
-        });
-    }
-}
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index 40265c4..0f6671d 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -188,7 +188,7 @@
                     SYS_UI_NAVIGATION_MODE.removeModeChangeListener(listener));
 
             Wait.atMost(() -> "Navigation mode didn't change to " + expectedMode,
-                    () -> currentSysUiNavigationMode() == expectedMode, 60000 /* b/148422894 */,
+                    () -> currentSysUiNavigationMode() == expectedMode, WAIT_TIME_MS,
                     launcher);
             // b/139137636
 //            assertTrue(launcher, "Navigation mode didn't change to " + expectedMode,
@@ -202,7 +202,7 @@
         Wait.atMost(() -> "Switching nav mode: "
                         + launcher.getNavigationModeMismatchError(),
                 () -> launcher.getNavigationModeMismatchError() == null,
-                60000 /* b/148422894 */, launcher);
+                WAIT_TIME_MS, launcher);
         AbstractLauncherUiTest.checkDetectedLeaks(launcher);
         return true;
     }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index bf093fd..ecd4e2b 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.tapl.Background;
 import com.android.launcher3.tapl.LauncherInstrumentation.NavigationModel;
 import com.android.launcher3.tapl.Overview;
+import com.android.launcher3.tapl.OverviewActions;
 import com.android.launcher3.tapl.OverviewTask;
 import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.ui.TaplTestsLauncher3;
@@ -68,11 +69,14 @@
         });
     }
 
-    private void startTestApps() throws Exception {
+    public static void startTestApps() throws Exception {
         startAppFast(getAppPackageName());
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
         startTestActivity(2);
+    }
 
+    private void startTestAppsWithCheck() throws Exception {
+        startTestApps();
         executeOnLauncher(launcher -> assertTrue(
                 "Launcher activity is the top activity; expecting another activity to be the top "
                         + "one",
@@ -105,7 +109,7 @@
     @Test
     @PortraitLandscape
     public void testOverview() throws Exception {
-        startTestApps();
+        startTestAppsWithCheck();
         // mLauncher.pressHome() also tests an important case of pressing home while in background.
         Overview overview = mLauncher.pressHome().switchToOverview();
         assertTrue("Launcher internal state didn't switch to Overview",
@@ -189,6 +193,22 @@
                         0, getTaskCount(launcher)));
     }
 
+    /**
+     * Smoke test for action buttons: Presses all the buttons and makes sure no crashes occur.
+     */
+    @Test
+    @NavigationModeSwitch
+    @PortraitLandscape
+    public void testOverviewActions() throws Exception {
+        if (mLauncher.getNavigationModel() != NavigationModel.TWO_BUTTON) {
+            startTestAppsWithCheck();
+            OverviewActions actionsView =
+                    mLauncher.pressHome().switchToOverview().getOverviewActions();
+            actionsView.clickAndDismissScreenshot();
+            actionsView.clickAndDismissShare();
+        }
+    }
+
     private int getCurrentOverviewPage(Launcher launcher) {
         return launcher.<RecentsView>getOverviewPanel().getCurrentPage();
     }
diff --git a/res/drawable/drag_handle_indicator_no_shadow.xml b/res/drawable/drag_handle_indicator_no_shadow.xml
deleted file mode 100644
index 341e60c..0000000
--- a/res/drawable/drag_handle_indicator_no_shadow.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="@dimen/vertical_drag_handle_width"
-    android:height="@dimen/vertical_drag_handle_height"
-    android:viewportWidth="18.0"
-    android:viewportHeight="6.0"
-    android:tint="?attr/workspaceTextColor" >
-
-    <path
-        android:pathData="M17,6c-0.15,0-0.3-0.03-0.45-0.11L9,2.12L1.45,5.89c-0.5,0.25-1.09,
-        0.05-1.34-0.45S0.06,4.35,0.55,4.11l8-4c0.28-0.14,0.61-0.14,0.89,0l8,4c0.49,0.25,0.69,
-        0.85,0.45,1.34C17.72,5.8,17.37,6,17,6z"
-        android:fillColor="@android:color/white" />
-</vector>
diff --git a/res/drawable-v24/drag_handle_indicator_shadow.xml b/res/layout/floating_surface_view.xml
similarity index 74%
rename from res/drawable-v24/drag_handle_indicator_shadow.xml
rename to res/layout/floating_surface_view.xml
index 774bc38..434e84f 100644
--- a/res/drawable-v24/drag_handle_indicator_shadow.xml
+++ b/res/layout/floating_surface_view.xml
@@ -5,7 +5,7 @@
      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
+          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,
@@ -13,7 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.graphics.ShadowDrawable
+<com.android.launcher3.views.FloatingSurfaceView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:src="@drawable/drag_handle_indicator_no_shadow"
-    android:elevation="@dimen/vertical_drag_handle_elevation" />
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content" />
diff --git a/res/layout/home_settings.xml b/res/layout/home_settings.xml
new file mode 100644
index 0000000..0f2461a
--- /dev/null
+++ b/res/layout/home_settings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <EditText
+        android:id="@+id/filter_box"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="@dimen/developer_options_filter_margins"
+        android:hint="@string/developer_options_filter_hint"
+        android:visibility="gone"
+        />
+
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:id="@android:id/list_container"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/launcher.xml b/res/layout/launcher.xml
index a137908..0c18c8a 100644
--- a/res/layout/launcher.xml
+++ b/res/layout/launcher.xml
@@ -28,6 +28,12 @@
         android:clipToPadding="false"
         android:importantForAccessibility="no">
 
+        <com.android.launcher3.views.AccessibilityActionsView
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:contentDescription="@string/home_screen"
+            />
+
         <!-- The workspace contains 5 screens of cells -->
         <!-- DO NOT CHANGE THE ID -->
         <com.android.launcher3.Workspace
diff --git a/res/layout/search_section_title.xml b/res/layout/search_section_title.xml
new file mode 100644
index 0000000..9ab27c1
--- /dev/null
+++ b/res/layout/search_section_title.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+          android:id="@+id/section_title"
+          android:textSize="14sp"
+          android:fontFamily="@style/TextHeadline"
+          android:layout_width="wrap_content"
+          android:textColor="?android:attr/textColorPrimary"
+          android:layout_marginLeft="16dp"
+          android:layout_marginRight="16dp"
+          android:paddingTop="8dp"
+          android:layout_height="wrap_content"/>
\ No newline at end of file
diff --git a/res/values/config.xml b/res/values/config.xml
index 75fcc90..dc8bdff 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -70,6 +70,7 @@
     <string name="test_information_handler_class" translatable="false"></string>
     <string name="launcher_activity_logic_class" translatable="false"></string>
     <string name="prediction_model_class" translatable="false"></string>
+    <string name="model_delegate_class" translatable="false"></string>
 
     <!-- View ID to use for QSB widget -->
     <item type="id" name="qsb_widget" />
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 947e635..f59f02f 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -40,14 +40,6 @@
     <dimen name="workspace_page_indicator_line_height">1dp</dimen>
     <dimen name="workspace_page_indicator_overlap_workspace">0dp</dimen>
 
-    <!-- Hotseat/all-apps scrim -->
-    <dimen name="all_apps_scrim_blur">4dp</dimen>
-    <dimen name="vertical_drag_handle_width">18dp</dimen>
-    <dimen name="vertical_drag_handle_height">6dp</dimen>
-    <dimen name="vertical_drag_handle_elevation">1dp</dimen>
-    <dimen name="vertical_drag_handle_touch_size">48dp</dimen>
-    <dimen name="vertical_drag_handle_padding_in_vertical_bar_layout">16dp</dimen>
-
 <!-- Drop target bar -->
     <dimen name="dynamic_grid_drop_target_size">48dp</dimen>
     <dimen name="drop_target_vertical_gap">20dp</dimen>
@@ -247,6 +239,9 @@
     <dimen name="snackbar_min_text_size">12sp</dimen>
     <dimen name="snackbar_max_text_size">14sp</dimen>
 
+<!-- Developer Options -->
+    <dimen name="developer_options_filter_margins">10dp</dimen>
+
 <!-- Theming related -->
     <dimen name="default_dialog_corner_radius">8dp</dimen>
 
diff --git a/res/values/drawables.xml b/res/values/drawables.xml
index 7d63142..9c57ec1 100644
--- a/res/values/drawables.xml
+++ b/res/values/drawables.xml
@@ -18,5 +18,4 @@
     <drawable name="ic_remove_shadow">@drawable/ic_remove_no_shadow</drawable>
     <drawable name="ic_uninstall_shadow">@drawable/ic_uninstall_no_shadow</drawable>
     <drawable name="ic_block_shadow">@drawable/ic_block_no_shadow</drawable>
-    <drawable name="all_apps_arrow_shadow">@drawable/drag_handle_indicator_no_shadow</drawable>
 </resources>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 935bb40..ef47eef 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -36,7 +36,7 @@
     <!-- Message shown when a shortcut is not available. It could have been temporarily disabled and may start working again after some time. -->
     <string name="shortcut_not_available">Shortcut isn\'t available</string>
     <!-- User visible name for the launcher/home screen. [CHAR_LIMIT=30] -->
-    <string name="home_screen">Home screen</string>
+    <string name="home_screen">Home</string>
     <!-- Label for showing custom action list of a shortcut or widget. [CHAR_LIMIT=30] -->
     <string name="custom_actions">Custom actions</string>
 
@@ -67,6 +67,10 @@
     <!-- Label for an icon representing any generic app. [CHAR_LIMIT=50] -->
     <string name="label_application">App</string>
 
+    <!--All apps Search-->
+    <!-- Section title for apps [CHAR_LIMIT=50] -->
+    <string name="search_corpus_apps">Apps</string>
+
     <!-- Popup items -->
     <!-- Text to display as the header above notifications. [CHAR_LIMIT=30] -->
     <string name="notifications_header">Notifications</string>
@@ -88,10 +92,6 @@
     <string name="all_apps_button_personal_label">Personal apps list</string>
     <string name="all_apps_button_work_label">Work apps list</string>
 
-    <!-- Label for button in all applications label to go back home (to the workspace / desktop)
-         for accessibilty (spoken when the button gets focus). -->
-    <string name="all_apps_home_button_label">Home</string>
-
     <!-- Label for remove drop target (from the homescreen only).
          May appear next to uninstall_drop_target_label [CHAR_LIMIT=20] -->
     <string name="remove_drop_target_label">Remove</string>
@@ -348,7 +348,8 @@
     <!-- content description for paused work apps list -->
     <string name="work_apps_paused_content_description">Work profile is paused. Work apps can\’t send you notifications, use your battery, or access your location</string>
 
-
+    <!-- A hint shown in launcher settings develop options filter box -->
+    <string name="developer_options_filter_hint">Filter</string>
 
     <!-- A tip shown pointing at work toggle -->
     <string name="work_switch_tip">Pause work apps and notifications</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 25f21f3..3b9532e 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -149,6 +149,15 @@
 
     <style name="HomeSettingsTheme" parent="@android:style/Theme.DeviceDefault.Settings">
         <item name="android:navigationBarColor">@android:color/transparent</item>
+        <item name="preferenceTheme">@style/HomeSettingsPreferenceTheme</item>
+    </style>
+
+    <style name="HomeSettingsPreferenceTheme" parent="@style/PreferenceThemeOverlay.v14.Material">
+        <item name="preferenceFragmentCompatStyle">@style/HomeSettingsFragmentCompatStyle</item>
+    </style>
+
+    <style name="HomeSettingsFragmentCompatStyle" parent="@style/PreferenceFragment.Material">
+        <item name="android:layout">@layout/home_settings</item>
     </style>
 
     <!--
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index cd27a2d..ce37a30 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -62,7 +62,8 @@
             TYPE_ALL_APPS_EDU,
 
             TYPE_TASK_MENU,
-            TYPE_OPTIONS_POPUP
+            TYPE_OPTIONS_POPUP,
+            TYPE_ICON_SURFACE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface FloatingViewType {}
@@ -80,16 +81,18 @@
     // Popups related to quickstep UI
     public static final int TYPE_TASK_MENU = 1 << 10;
     public static final int TYPE_OPTIONS_POPUP = 1 << 11;
+    public static final int TYPE_ICON_SURFACE = 1 << 12;
 
     public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP
             | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET
             | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU
-            | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU;
+            | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU
+            | TYPE_ICON_SURFACE;
 
     // Type of popups which should be kept open during launcher rebind
     public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET
             | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE
-            | TYPE_ALL_APPS_EDU;
+            | TYPE_ALL_APPS_EDU | TYPE_ICON_SURFACE;
 
     // Usually we show the back button when a floating view is open. Instead, hide for these types.
     public static final int TYPE_HIDE_BACK_BUTTON = TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 296fb7a..112126b 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -81,7 +81,7 @@
         super.onCreate(savedInstanceState);
 
 
-        mIsSafeModeEnabled = TraceHelper.whitelistIpcs("isSafeMode",
+        mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
                 () -> getPackageManager().isSafeMode());
         DefaultDisplay.INSTANCE.get(this).addChangeListener(this);
 
diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java
index 41eeb78..c55b46b 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -16,11 +16,8 @@
 
 package com.android.launcher3;
 
-import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
-
 import android.content.Context;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -29,8 +26,6 @@
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 
 
@@ -183,10 +178,6 @@
     public void onScrollStateChanged(int state) {
         super.onScrollStateChanged(state);
 
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onScrollStateChanged: " + state);
-        }
-
         if (state == SCROLL_STATE_IDLE) {
             AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext());
         }
@@ -196,23 +187,5 @@
     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(info);
         if (isLayoutSuppressed()) info.setScrollable(false);
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS,
-                    "onInitializeAccessibilityNodeInfo, scrollable: " + info.isScrollable());
-        }
-    }
-
-    @Override
-    public void setLayoutFrozen(boolean frozen) {
-        final boolean changing = frozen != isLayoutSuppressed();
-        super.setLayoutFrozen(frozen);
-        if (changing) {
-            if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-                Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "setLayoutFrozen " + frozen
-                        + " @ " + Log.getStackTraceString(new Throwable()));
-                ActivityContext.lookupContext(getContext()).getDragLayer()
-                        .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED);
-            }
-        }
     }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 60b6da6..198f13d 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -331,10 +331,7 @@
     public boolean onTouchEvent(MotionEvent event) {
         // ignore events if they happen in padding area
         if (event.getAction() == MotionEvent.ACTION_DOWN
-                && (event.getY() < getPaddingTop()
-                || event.getX() < getPaddingLeft()
-                || event.getY() > getHeight() - getPaddingBottom()
-                || event.getX() > getWidth() - getPaddingRight())) {
+                && shouldIgnoreTouchDown(event.getX(), event.getY())) {
             return false;
         }
         if (isLongClickable()) {
@@ -347,6 +344,16 @@
         }
     }
 
+    /**
+     * Returns true if the touch down at the provided position be ignored
+     */
+    protected boolean shouldIgnoreTouchDown(float x, float y) {
+        return y < getPaddingTop()
+                || x < getPaddingLeft()
+                || y > getHeight() - getPaddingBottom()
+                || x > getWidth() - getPaddingRight();
+    }
+
     void setStayPressed(boolean stayPressed) {
         mStayPressed = stayPressed;
         refreshDrawableState();
@@ -607,6 +614,9 @@
     @Override
     public void setIconVisible(boolean visible) {
         mIsIconVisible = visible;
+        if (!mIsIconVisible) {
+            resetIconScale();
+        }
         Drawable icon = visible ? mIcon : new ColorDrawable(Color.TRANSPARENT);
         applyCompoundDrawables(icon);
     }
@@ -746,11 +756,14 @@
 
     @Override
     public SafeCloseable prepareDrawDragView() {
-        if (getIcon() instanceof FastBitmapDrawable) {
-            FastBitmapDrawable icon = (FastBitmapDrawable) getIcon();
-            icon.setScale(1f);
-        }
+        resetIconScale();
         setForceHideDot(true);
         return () -> { };
     }
+
+    private void resetIconScale() {
+        if (mIcon instanceof FastBitmapDrawable) {
+            ((FastBitmapDrawable) mIcon).setScale(1f);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/GestureNavContract.java b/src/com/android/launcher3/GestureNavContract.java
new file mode 100644
index 0000000..2a7e629
--- /dev/null
+++ b/src/com/android/launcher3/GestureNavContract.java
@@ -0,0 +1,101 @@
+/*
+ * 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;
+
+import static android.content.Intent.EXTRA_COMPONENT_NAME;
+import static android.content.Intent.EXTRA_USER;
+
+import android.annotation.TargetApi;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.SurfaceControl;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Class to encapsulate the handshake protocol between Launcher and gestureNav.
+ */
+public class GestureNavContract {
+
+    private static final String TAG = "GestureNavContract";
+
+    public static final String EXTRA_GESTURE_CONTRACT = "gesture_nav_contract_v1";
+    public static final String EXTRA_ICON_POSITION = "gesture_nav_contract_icon_position";
+    public static final String EXTRA_ICON_SURFACE = "gesture_nav_contract_surface_control";
+    public static final String EXTRA_REMOTE_CALLBACK = "android.intent.extra.REMOTE_CALLBACK";
+
+    public final ComponentName componentName;
+    public final UserHandle user;
+
+    private final Message mCallback;
+
+    public GestureNavContract(ComponentName componentName, UserHandle user, Message callback) {
+        this.componentName = componentName;
+        this.user = user;
+        this.mCallback = callback;
+    }
+
+    /**
+     * Sends the position information to the receiver
+     */
+    @TargetApi(Build.VERSION_CODES.R)
+    public void sendEndPosition(RectF position, @Nullable SurfaceControl surfaceControl) {
+        Bundle result = new Bundle();
+        result.putParcelable(EXTRA_ICON_POSITION, position);
+        result.putParcelable(EXTRA_ICON_SURFACE, surfaceControl);
+
+        Message callback = Message.obtain();
+        callback.copyFrom(mCallback);
+        callback.setData(result);
+
+        try {
+            callback.replyTo.send(callback);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending icon position", e);
+        }
+    }
+
+    /**
+     * Clears and returns the GestureNavContract if it was present in the intent.
+     */
+    public static GestureNavContract fromIntent(Intent intent) {
+        if (!Utilities.ATLEAST_R) {
+            return null;
+        }
+        Bundle extras = intent.getBundleExtra(EXTRA_GESTURE_CONTRACT);
+        if (extras == null) {
+            return null;
+        }
+        intent.removeExtra(EXTRA_GESTURE_CONTRACT);
+
+        ComponentName componentName = extras.getParcelable(EXTRA_COMPONENT_NAME);
+        UserHandle userHandle = extras.getParcelable(EXTRA_USER);
+        Message callback = extras.getParcelable(EXTRA_REMOTE_CALLBACK);
+
+        if (componentName != null && userHandle != null && callback != null
+                && callback.replyTo != null) {
+            return new GestureNavContract(componentName, userHandle, callback);
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 2bf47fb..db1f1b0 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -21,6 +21,7 @@
 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
+import static com.android.launcher3.AbstractFloatingView.TYPE_ICON_SURFACE;
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
 import static com.android.launcher3.AbstractFloatingView.TYPE_SNACKBAR;
 import static com.android.launcher3.InstallShortcutReceiver.FLAG_DRAG_AND_DROP;
@@ -118,7 +119,6 @@
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.ModelWriter;
 import com.android.launcher3.model.data.AppInfo;
@@ -168,6 +168,7 @@
 import com.android.launcher3.util.UiThreadHelper;
 import com.android.launcher3.util.ViewOnDrawExecutor;
 import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.FloatingSurfaceView;
 import com.android.launcher3.views.OptionsPopupView;
 import com.android.launcher3.views.ScrimView;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
@@ -451,10 +452,10 @@
                     mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
                 } else if (finalState == OVERVIEW || finalState == OVERVIEW_PEEK) {
                     mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
-                    mScrimView.getAlphaProperty(SCRIM_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
+                    mScrimView.setAlpha(alpha);
                 } else {
                     mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(1f);
-                    mScrimView.getAlphaProperty(SCRIM_VIEW_ALPHA_CHANNEL_INDEX).setValue(1f);
+                    mScrimView.setAlpha(1f);
                 }
             }
         });
@@ -509,6 +510,7 @@
     public void onEnterAnimationComplete() {
         super.onEnterAnimationComplete();
         mRotationHelper.setCurrentTransitionRequest(REQUEST_NONE);
+        AbstractFloatingView.closeOpenViews(this, false, TYPE_ICON_SURFACE);
     }
 
     @Override
@@ -550,7 +552,7 @@
             mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
         } else if (state == OVERVIEW || state == OVERVIEW_PEEK) {
             mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
-            mScrimView.getAlphaProperty(SCRIM_VIEW_ALPHA_CHANNEL_INDEX).setValue(alpha);
+            mScrimView.setAlpha(alpha);
         }
     }
 
@@ -908,14 +910,12 @@
         logStopAndResume(Action.Command.RESUME);
         getUserEventDispatcher().startSession();
 
-        AppLaunchTracker.INSTANCE.get(this).onReturnedToHome();
-
         // Process any items that were added while Launcher was away.
         InstallShortcutReceiver.disableAndFlushInstallQueue(
                 InstallShortcutReceiver.FLAG_ACTIVITY_PAUSED, this);
 
         // Refresh shortcuts if the permission changed.
-        mModel.refreshShortcutsIfRequired();
+        mModel.validateModelDataOnResume();
 
         // Set the notification listener and fetch updated notifications when we resume
         NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
@@ -1450,6 +1450,7 @@
                 mLauncherCallbacks.onHomeIntent(internalStateHandled);
             }
             mOverlayManager.hideOverlay(isStarted() && !isForceInvisible());
+            handleGestureContract(intent);
         } else if (Intent.ACTION_ALL_APPS.equals(intent.getAction())) {
             getStateManager().goToState(ALL_APPS, alreadyOnHome);
         }
@@ -1458,6 +1459,17 @@
     }
 
     /**
+     * Handles gesture nav contract
+     */
+    protected void handleGestureContract(Intent intent) {
+        GestureNavContract gnc = GestureNavContract.fromIntent(intent);
+        if (gnc != null) {
+            AbstractFloatingView.closeOpenViews(this, false, TYPE_ICON_SURFACE);
+            FloatingSurfaceView.show(this, gnc);
+        }
+    }
+
+    /**
      * Hides the keyboard if visible
      */
     public void hideKeyboard() {
@@ -1535,7 +1547,6 @@
         mOverlayManager.onActivityDestroyed(this);
         mAppTransitionManager.unregisterRemoteAnimations();
         mUserChangedCallbackCloseable.close();
-        mAllAppsController.onActivityDestroyed();
     }
 
     public LauncherAccessibilityDelegate getAccessibilityDelegate() {
@@ -1922,7 +1933,7 @@
         // Populate event with a fake title based on the current state.
         // TODO: When can workspace be null?
         text.add(mWorkspace == null
-                ? getString(R.string.all_apps_home_button_label)
+                ? getString(R.string.home_screen)
                 : mStateManager.getState().getDescription(this));
         return result;
     }
@@ -2480,7 +2491,7 @@
      * @param updated list of shortcuts which have changed.
      */
     @Override
-    public void bindWorkspaceItemsChanged(ArrayList<WorkspaceItemInfo> updated) {
+    public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) {
         if (!updated.isEmpty()) {
             mWorkspace.updateShortcuts(updated);
         }
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 53e5274..e3fe87d 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -128,7 +128,7 @@
         mInvariantDeviceProfile = InvariantDeviceProfile.INSTANCE.get(context);
         mIconCache = new IconCache(mContext, mInvariantDeviceProfile, iconCacheFileName);
         mWidgetCache = new WidgetPreviewLoader(mContext, mIconCache);
-        mModel = new LauncherModel(this, mIconCache, AppFilter.newInstance(mContext));
+        mModel = new LauncherModel(context, this, mIconCache, AppFilter.newInstance(mContext));
         mPredictionModel = PredictionModel.newInstance(mContext);
     }
 
@@ -157,6 +157,7 @@
      * Call from Application.onTerminate(), which is not guaranteed to ever be called.
      */
     public void onTerminate() {
+        mModel.destroy();
         if (mModelChangeReceiver != null) {
             mContext.unregisterReceiver(mModelChangeReceiver);
         }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index ff4b545..e2568d5 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -20,7 +20,6 @@
 import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
 
 import android.content.Context;
 import android.content.Intent;
@@ -46,6 +45,7 @@
 import com.android.launcher3.model.CacheDataUpdatedTask;
 import com.android.launcher3.model.LoaderResults;
 import com.android.launcher3.model.LoaderTask;
+import com.android.launcher3.model.ModelDelegate;
 import com.android.launcher3.model.ModelWriter;
 import com.android.launcher3.model.PackageInstallStateChangedTask;
 import com.android.launcher3.model.PackageUpdatedTask;
@@ -112,20 +112,22 @@
      */
     private final BgDataModel mBgDataModel = new BgDataModel();
 
+    private final ModelDelegate mModelDelegate;
+
     // Runnable to check if the shortcuts permission has changed.
-    private final Runnable mShortcutPermissionCheckRunnable = new Runnable() {
+    private final Runnable mDataValidationCheck = new Runnable() {
         @Override
         public void run() {
-            if (mModelLoaded && hasShortcutsPermission(mApp.getContext())
-                    != mBgAllAppsList.hasShortcutHostPermission()) {
-                forceReload();
+            if (mModelLoaded) {
+                mModelDelegate.validateData();
             }
         }
     };
 
-    LauncherModel(LauncherAppState app, IconCache iconCache, AppFilter appFilter) {
+    LauncherModel(Context context, LauncherAppState app, IconCache iconCache, AppFilter appFilter) {
         mApp = app;
         mBgAllAppsList = new AllAppsList(iconCache, appFilter);
+        mModelDelegate = ModelDelegate.newInstance(context, app, mBgAllAppsList, mBgDataModel);
     }
 
     /**
@@ -217,6 +219,13 @@
         }
     }
 
+    /**
+     * Called when the model is destroyed
+     */
+    public void destroy() {
+        MODEL_EXECUTOR.execute(mModelDelegate::destroy);
+    }
+
     public void onBroadcastIntent(Intent intent) {
         if (DEBUG_RECEIVER) Log.d(TAG, "onReceive intent=" + intent);
         final String action = intent.getAction();
@@ -372,7 +381,8 @@
     public void startLoaderForResults(LoaderResults results) {
         synchronized (mLock) {
             stopLoader();
-            mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, mBgDataModel, results);
+            mLoaderTask = new LoaderTask(
+                    mApp, mBgAllAppsList, mBgDataModel, mModelDelegate, results);
 
             // Always post the loader task, instead of running directly (even on same thread) so
             // that we exit any nested synchronized blocks
@@ -491,9 +501,9 @@
      * Current implementation simply reloads the workspace, but it can be optimized to
      * use partial updates similar to {@link UserCache}
      */
-    public void refreshShortcutsIfRequired() {
-        MODEL_EXECUTOR.getHandler().removeCallbacks(mShortcutPermissionCheckRunnable);
-        MODEL_EXECUTOR.post(mShortcutPermissionCheckRunnable);
+    public void validateModelDataOnResume() {
+        MODEL_EXECUTOR.getHandler().removeCallbacks(mDataValidationCheck);
+        MODEL_EXECUTOR.post(mDataValidationCheck);
     }
 
     /**
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index e29faac..e13d89f 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -1448,11 +1448,8 @@
         int minDistanceFromScreenCenterIndex = -1;
         final int childCount = getChildCount();
         for (int i = 0; i < childCount; ++i) {
-            View layout = getPageAt(i);
-            int childSize = mOrientationHandler.getMeasuredSize(layout);
-            int halfChildSize = (childSize / 2);
-            int childCenter = getChildOffset(i) + halfChildSize;
-            int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
+            int distanceFromScreenCenter = Math.abs(
+                    getDisplacementFromScreenCenter(i, screenCenter));
             if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
                 minDistanceFromScreenCenter = distanceFromScreenCenter;
                 minDistanceFromScreenCenterIndex = i;
@@ -1461,6 +1458,20 @@
         return minDistanceFromScreenCenterIndex;
     }
 
+    private int getDisplacementFromScreenCenter(int childIndex, int screenCenter) {
+        View layout = getPageAt(childIndex);
+        int childSize = mOrientationHandler.getMeasuredSize(layout);
+        int halfChildSize = (childSize / 2);
+        int childCenter = getChildOffset(childIndex) + halfChildSize;
+        return childCenter - screenCenter;
+    }
+
+    protected int getDisplacementFromScreenCenter(int childIndex) {
+        int pageOrientationSize = mOrientationHandler.getMeasuredSize(this);
+        int screenCenter = mOrientationHandler.getPrimaryScroll(this) + (pageOrientationSize / 2);
+        return getDisplacementFromScreenCenter(childIndex, screenCenter);
+    }
+
     protected void snapToDestination() {
         snapToPage(getPageNearestToCenterOfScreen(), getPageSnapDuration());
     }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 1441e0b..3be9ac7 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -115,6 +115,7 @@
 
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
 import java.util.function.Predicate;
 
 /**
@@ -309,7 +310,9 @@
             // In portrait, we want the pages spaced such that there is no
             // overhang of the previous / next page into the current page viewport.
             // We assume symmetrical padding in portrait mode.
-            setPageSpacing(Math.max(grid.edgeMarginPx, padding.left + 1));
+            int maxInsets = Math.max(insets.left, insets.right);
+            int maxPadding = Math.max(grid.edgeMarginPx, padding.left + 1);
+            setPageSpacing(Math.max(maxInsets, maxPadding));
         }
 
 
@@ -3085,7 +3088,7 @@
         return false;
     }
 
-    void updateShortcuts(ArrayList<WorkspaceItemInfo> shortcuts) {
+    void updateShortcuts(List<WorkspaceItemInfo> shortcuts) {
         final HashSet<WorkspaceItemInfo> updates = new HashSet<>(shortcuts);
         ItemOperator op = (info, v) -> {
             if (v instanceof BubbleTextView && updates.contains(info)) {
@@ -3260,7 +3263,7 @@
         }
         if (nScreens == 0) {
             // When the workspace is not loaded, we do not know how many screen will be bound.
-            return getContext().getString(R.string.all_apps_home_button_label);
+            return getContext().getString(R.string.home_screen);
         }
         return getContext().getString(R.string.workspace_scroll_format, page + 1, nScreens);
     }
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index d653160..dec92df 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -66,6 +66,8 @@
     // A divider that separates the apps list and the search market button
     public static final int VIEW_TYPE_ALL_APPS_DIVIDER = 1 << 4;
 
+    public static final int VIEW_TYPE_SEARCH_CORPUS_TITLE =  1 << 5;
+
     // Common view type masks
     public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER;
     public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON;
@@ -274,6 +276,9 @@
             case VIEW_TYPE_ALL_APPS_DIVIDER:
                 return new ViewHolder(mLayoutInflater.inflate(
                         R.layout.all_apps_divider, parent, false));
+            case VIEW_TYPE_SEARCH_CORPUS_TITLE:
+                return new ViewHolder(
+                        mLayoutInflater.inflate(R.layout.search_section_title, parent, false));
             default:
                 throw new RuntimeException("Unexpected view type");
         }
@@ -302,6 +307,10 @@
                     searchView.setVisibility(View.GONE);
                 }
                 break;
+            case VIEW_TYPE_SEARCH_CORPUS_TITLE:
+                TextView titleView = (TextView) holder.itemView;
+                titleView.setText(mApps.getAdapterItems().get(position).searchSectionInfo.getTitle(
+                        titleView.getContext()));
             case VIEW_TYPE_ALL_APPS_DIVIDER:
                 // nothing to do
                 break;
diff --git a/src/com/android/launcher3/allapps/AllAppsInsetTransitionController.java b/src/com/android/launcher3/allapps/AllAppsInsetTransitionController.java
new file mode 100644
index 0000000..d7af5f1
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsInsetTransitionController.java
@@ -0,0 +1,197 @@
+/*
+ * 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.allapps;
+
+import android.graphics.Insets;
+import android.os.Build;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowInsetsAnimationControlListener;
+import android.view.WindowInsetsAnimationController;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.os.BuildCompat;
+
+/**
+ * Handles IME over all apps to be synchronously transitioning along with the passed in
+ * root inset.
+ */
+public class AllAppsInsetTransitionController {
+
+    private static final boolean DEBUG = false;
+    private static final String TAG = "AllAppsInsetTransitionController";
+    private static final Interpolator LINEAR = new LinearInterpolator();
+
+    private WindowInsetsAnimationController mAnimationController;
+    private WindowInsetsAnimationControlListener mCurrentRequest;
+
+    private float mAllAppsHeight;
+
+    private int mDownInsetBottom;
+    private boolean mShownAtDown;
+
+    private int mHiddenInsetBottom;
+    private int mShownInsetBottom;
+
+    private float mDown, mCurrent;
+    private View mApps;
+
+    public AllAppsInsetTransitionController(float allAppsHeight, View appsView) {
+        mAllAppsHeight = allAppsHeight;
+        mApps = appsView;
+    }
+
+    public void hide() {
+        if (!BuildCompat.isAtLeastR()) return;
+
+        WindowInsets insets = mApps.getRootWindowInsets();
+        if (insets == null) return;
+
+        if (insets.isVisible(WindowInsets.Type.ime())) {
+            mApps.getWindowInsetsController().hide(WindowInsets.Type.ime());
+        }
+    }
+
+    /**
+     * Initializes member variables and requests for the {@link WindowInsetsAnimationController}
+     * object.
+     *
+     * @param progress value between 0..1
+     */
+    @RequiresApi(api = Build.VERSION_CODES.R)
+    public void onDragStart(float progress) {
+        if (!BuildCompat.isAtLeastR()) return;
+        onAnimationEnd(progress);
+
+        mDown = progress * mAllAppsHeight;
+
+        // Below two values are sometimes incorrect. Possibly a platform bug
+        mDownInsetBottom = mApps.getRootWindowInsets().getInsets(WindowInsets.Type.ime()).bottom;
+        mShownAtDown = mApps.getRootWindowInsets().isVisible(WindowInsets.Type.ime());
+
+        // override this value based on what it should actually be.
+        mShownAtDown = Float.compare(progress, 1f) == 0 ? false : true;
+        mDownInsetBottom = mShownAtDown ? mShownInsetBottom : mHiddenInsetBottom;
+        if (DEBUG) {
+            Log.d(TAG, "\nonDragStart mDownInsets=" + mDownInsetBottom
+                    + " mShownAtDown =" + mShownAtDown);
+        }
+
+        mApps.getWindowInsetsController().controlWindowInsetsAnimation(
+                WindowInsets.Type.ime(), -1 /* no predetermined duration */, LINEAR, null,
+                mCurrentRequest = new WindowInsetsAnimationControlListener() {
+
+                    @Override
+                    public void onReady(WindowInsetsAnimationController controller, int types) {
+                        if (DEBUG) {
+                            Log.d(TAG, "Listener.onReady " + (mCurrentRequest == this));
+                        }
+                        if (mCurrentRequest == this) {
+                            mAnimationController = controller;
+                        } else {
+                            controller.finish(mShownAtDown);
+                        }
+                    }
+
+                    @Override
+                    public void onFinished(WindowInsetsAnimationController controller) {
+                        // when screen lock happens, then this method get called
+                        mAnimationController.finish(false);
+                        mAnimationController = null;
+                        if (DEBUG) {
+                            Log.d(TAG, "Listener.onFinished ctrl=" + controller);
+                        }
+                    }
+
+                    @Override
+                    public void onCancelled(@Nullable WindowInsetsAnimationController controller) {
+                        mAnimationController = null;
+                        if (controller != null) {
+                            controller.finish(mShownAtDown);
+                        }
+                        if (DEBUG) {
+                            Log.d(TAG, "Listener.onCancelled ctrl=" + controller);
+                        }
+                    }
+                });
+    }
+
+    /**
+     * Handles the translation using the progress.
+     *
+     * @param progress value between 0..1
+     */
+    @RequiresApi(api = 30)
+    public void setProgress(float progress) {
+        if (!BuildCompat.isAtLeastR()) return;
+        // progress that equals to 0 or 1 is error prone. Do not use them.
+        // Instead use onDragStart and onAnimationEnd
+        if (mAnimationController == null || progress <= 0f || progress >= 1f) return;
+
+        mCurrent = progress * mAllAppsHeight;
+        mHiddenInsetBottom = mAnimationController.getHiddenStateInsets().bottom; // 0
+        mShownInsetBottom = mAnimationController.getShownStateInsets().bottom; // 1155
+
+        int shift = mShownAtDown ? 0 : (int) (mAllAppsHeight - mShownInsetBottom);
+
+        int inset = (int) (mDownInsetBottom + (mDown - mCurrent) - shift);
+
+        if (DEBUG) {
+            Log.d(TAG, "updateInset mCurrent=" + mCurrent + " mDown="
+                    + mDown + " hidden=" + mHiddenInsetBottom
+                    + " shown=" + mShownInsetBottom
+                    + " mDownInsets.bottom=" + mDownInsetBottom + " inset:" + inset
+                    + " shift: " + shift);
+        }
+        final int start = mShownAtDown ? mShownInsetBottom : mHiddenInsetBottom;
+        final int end = mShownAtDown ? mHiddenInsetBottom : mShownInsetBottom;
+        inset = Math.max(inset, mHiddenInsetBottom);
+        inset = Math.min(inset, mShownInsetBottom);
+        Log.d(TAG, "updateInset inset:" + inset);
+
+        mAnimationController.setInsetsAndAlpha(
+                Insets.of(0, 0, 0, inset),
+                1f, (inset - start) / (float) (end - start));
+    }
+
+    /**
+     * Report to the animation controller that we no longer plan to translate anymore.
+     *
+     * @param progress value between 0..1
+     */
+    @RequiresApi(api = 30)
+    public void onAnimationEnd(float progress) {
+        if (DEBUG) {
+            Log.d(TAG, "endTranslation progress=" + progress
+                    + " mAnimationController=" + mAnimationController);
+        }
+
+        if (mAnimationController == null) return;
+
+        if (Float.compare(progress, 1f) == 0 /* bottom */) {
+            mAnimationController.finish(false /* gone */);
+        }
+        if (Float.compare(progress, 0f) == 0 /* top */) {
+            mAnimationController.finish(true /* show */);
+        }
+        mAnimationController = null;
+        mCurrentRequest = null;
+    }
+}
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index cbf02b7..640ef01 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -107,6 +107,7 @@
         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ALL_APPS_DIVIDER, 1);
         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET, 1);
         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ICON, approxRows * mNumAppsPerRow);
+        pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_CORPUS_TITLE, 1);
 
         mViewHeights.clear();
         mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, grid.allAppsCellHeightPx);
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index a9b030e..0268b96 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -1,10 +1,24 @@
+/*
+ * Copyright (C) 2015 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.allapps;
 
 import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT;
 import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
 import static com.android.launcher3.LauncherState.APPS_VIEW_ITEM_MASK;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.VERTICAL_SWIPE_INDICATOR;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.FINAL_FRAME;
 import static com.android.launcher3.anim.Interpolators.INSTANT;
@@ -14,31 +28,29 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_HEADER_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
+import static com.android.launcher3.util.SystemUiController.UI_STATE_ALLAPPS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
-import android.content.Context;
 import android.util.FloatProperty;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.animation.Interpolator;
 import android.widget.EditText;
 
+import androidx.core.os.BuildCompat;
+
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.views.ScrimView;
-import com.android.systemui.plugins.AllAppsSearchPlugin;
-import com.android.systemui.plugins.PluginListener;
 
 /**
  * Handles AllApps view transition.
@@ -51,7 +63,7 @@
  * closer to top or closer to the page indicator.
  */
 public class AllAppsTransitionController implements StateHandler<LauncherState>,
-        OnDeviceProfileChangeListener, PluginListener<AllAppsSearchPlugin> {
+        OnDeviceProfileChangeListener {
 
     public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
             new FloatProperty<AllAppsTransitionController>("allAppsProgress") {
@@ -85,10 +97,7 @@
     private float mProgress;        // [0, 1], mShiftRange * mProgress = shiftCurrent
 
     private float mScrollRangeDelta = 0;
-
-    // plugin related variables
-    private AllAppsSearchPlugin mPlugin;
-    private View mPluginContent;
+    private AllAppsInsetTransitionController mInsetController;
 
     public AllAppsTransitionController(Launcher l) {
         mLauncher = l;
@@ -103,6 +112,10 @@
         return mShiftRange;
     }
 
+    public AllAppsInsetTransitionController getInsetController() {
+        return mInsetController;
+    }
+
     @Override
     public void onDeviceProfileChanged(DeviceProfile dp) {
         mIsVerticalLayout = dp.isVerticalBarLayout();
@@ -130,8 +143,8 @@
         float shiftCurrent = progress * mShiftRange;
 
         mAppsView.setTranslationY(shiftCurrent);
-        if (mPlugin != null) {
-            mPlugin.setProgress(progress);
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+            mInsetController.setProgress(progress);
         }
     }
 
@@ -201,20 +214,13 @@
         Interpolator allAppsFade = config.getInterpolator(ANIM_ALL_APPS_FADE, LINEAR);
         Interpolator headerFade = config.getInterpolator(ANIM_ALL_APPS_HEADER_FADE, allAppsFade);
 
-        if (mPlugin == null) {
-            setter.setViewAlpha(mAppsView.getContentView(), hasAllAppsContent ? 1 : 0, allAppsFade);
-            setter.setViewAlpha(mAppsView.getScrollBar(), hasAllAppsContent ? 1 : 0, allAppsFade);
-            mAppsView.getFloatingHeaderView().setContentVisibility(hasHeaderExtra,
-                    hasAllAppsContent, setter, headerFade, allAppsFade);
-        } else {
-            setter.setViewAlpha(mPluginContent, hasAllAppsContent ? 1 : 0, allAppsFade);
-            setter.setViewAlpha(mAppsView.getContentView(), 0, allAppsFade);
-            setter.setViewAlpha(mAppsView.getScrollBar(), 0, allAppsFade);
-        }
-        mAppsView.getSearchUiManager().setContentVisibility(visibleElements, setter, allAppsFade);
 
-        setter.setInt(mScrimView, ScrimView.DRAG_HANDLE_ALPHA,
-                (visibleElements & VERTICAL_SWIPE_INDICATOR) != 0 ? 255 : 0, allAppsFade);
+        setter.setViewAlpha(mAppsView.getContentView(), hasAllAppsContent ? 1 : 0, allAppsFade);
+        setter.setViewAlpha(mAppsView.getScrollBar(), hasAllAppsContent ? 1 : 0, allAppsFade);
+        mAppsView.getFloatingHeaderView().setContentVisibility(hasHeaderExtra,
+                hasAllAppsContent, setter, headerFade, allAppsFade);
+
+        mAppsView.getSearchUiManager().setContentVisibility(visibleElements, setter, allAppsFade);
 
         // Set visibility of the container at the very beginning or end of the transition.
         setter.setViewAlpha(mAppsView, hasAnyVisibleItem ? 1 : 0,
@@ -228,8 +234,12 @@
     public void setupViews(AllAppsContainerView appsView, ScrimView scrimView) {
         mAppsView = appsView;
         mScrimView = scrimView;
-        PluginManagerWrapper.INSTANCE.get(mLauncher)
-                .addPluginListener(this, AllAppsSearchPlugin.class, false);
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && BuildCompat.isAtLeastR()) {
+            mInsetController = new AllAppsInsetTransitionController(mShiftRange, mAppsView);
+            mLauncher.getSystemUiController().updateUiState(UI_STATE_ALLAPPS,
+                    View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+        }
     }
 
     /**
@@ -252,47 +262,19 @@
         if (Float.compare(mProgress, 1f) == 0) {
             mAppsView.reset(false /* animate */);
         }
-        updatePluginAnimationEnd();
-    }
-
-    @Override
-    public void onPluginConnected(AllAppsSearchPlugin plugin, Context context) {
-        mPlugin = plugin;
-        mPluginContent = mLauncher.getLayoutInflater().inflate(
-                R.layout.all_apps_content_layout, mAppsView, false);
-        mAppsView.addView(mPluginContent);
-        mPluginContent.setAlpha(0f);
-        mPlugin.setup((ViewGroup) mPluginContent, mLauncher, mShiftRange);
-    }
-
-    @Override
-    public void onPluginDisconnected(AllAppsSearchPlugin plugin) {
-        mPlugin = null;
-        mAppsView.removeView(mPluginContent);
-    }
-
-    public void onActivityDestroyed() {
-        PluginManagerWrapper.INSTANCE.get(mLauncher).removePluginListener(this);
-    }
-
-    /** Used for the plugin to signal when drag starts happens
-     * @param toAllApps*/
-    public void onDragStart(boolean toAllApps) {
-        if (mPlugin == null) return;
-
-        if (toAllApps) {
-            EditText editText = mAppsView.getSearchUiManager().setTextSearchEnabled(true);
-            mPlugin.setEditText(editText);
-        }
-        mPlugin.onDragStart(toAllApps ? 1f : 0f);
-    }
-
-    private void updatePluginAnimationEnd() {
-        if (mPlugin == null) return;
-        mPlugin.onAnimationEnd(mProgress);
-        if (Float.compare(mProgress, 1f) == 0) {
-            mAppsView.getSearchUiManager().setTextSearchEnabled(false);
-            mPlugin.setEditText(null);
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && BuildCompat.isAtLeastR()) {
+            mInsetController.onAnimationEnd(mProgress);
+            if (Float.compare(mProgress, 0f) == 0) {
+                EditText editText = mAppsView.getSearchUiManager().getEditText();
+                if (editText != null) {
+                    editText.requestFocus();
+                }
+            }
+            if (Float.compare(mProgress, 1f) == 0) {
+                // Called when home gesture closes all apps container.
+                // TODO: should make the controller hide synchronously
+                mInsetController.hide();
+            }
         }
     }
 }
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 06209bb..266bfa2 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -19,8 +19,9 @@
 import android.content.Context;
 
 import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.allapps.search.SearchSectionInfo;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LabelComparator;
 
@@ -30,6 +31,7 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.stream.Collectors;
 
 /**
  * The alphabetically sorted list of applications.
@@ -82,6 +84,8 @@
         public AppInfo appInfo = null;
         // The index of this app not including sections
         public int appIndex = -1;
+        // Search section associated to result
+        public SearchSectionInfo searchSectionInfo = null;
 
         public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo,
                 int appIndex) {
@@ -114,6 +118,17 @@
             item.position = pos;
             return item;
         }
+
+        /**
+         * Factory method for search section title AdapterItem
+         */
+        public static AdapterItem asSearchTitle(SearchSectionInfo sectionInfo, int pos) {
+            AdapterItem item = new AdapterItem();
+            item.viewType = AllAppsGridAdapter.VIEW_TYPE_SEARCH_CORPUS_TITLE;
+            item.position = pos;
+            item.searchSectionInfo = sectionInfo;
+            return item;
+        }
     }
 
     private final BaseDraggingActivity mLauncher;
@@ -132,7 +147,7 @@
     private final boolean mIsWork;
 
     // The of ordered component names as a result of a search query
-    private ArrayList<ComponentKey> mSearchResults;
+    private ArrayList<AdapterItem> mSearchResults;
     private AllAppsGridAdapter mAdapter;
     private AppInfoComparator mAppNameComparator;
     private final int mNumAppsPerRow;
@@ -210,10 +225,10 @@
     }
 
     /**
-     * Sets the sorted list of filtered components.
+     * Sets results list for search
      */
-    public boolean setOrderedFilter(ArrayList<ComponentKey> f) {
-        if (mSearchResults != f) {
+    public boolean setSearchResults(ArrayList<AdapterItem> f) {
+        if (f == null || mSearchResults != f) {
             boolean same = mSearchResults != null && mSearchResults.equals(f);
             mSearchResults = f;
             onAppsUpdated();
@@ -298,35 +313,42 @@
 
         // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
         // ordered set of sections
-        for (AppInfo info : getFiltersAppInfos()) {
-            String sectionName = info.sectionName;
+        if (!hasFilter()) {
+            for (AppInfo info : mApps) {
+                String sectionName = info.sectionName;
 
-            // Create a new section if the section names do not match
-            if (!sectionName.equals(lastSectionName)) {
-                lastSectionName = sectionName;
-                lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
-                mFastScrollerSections.add(lastFastScrollerSectionInfo);
-            }
+                // Create a new section if the section names do not match
+                if (!sectionName.equals(lastSectionName)) {
+                    lastSectionName = sectionName;
+                    lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
+                    mFastScrollerSections.add(lastFastScrollerSectionInfo);
+                }
 
-            // Create an app item
-            AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++);
-            if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
-                lastFastScrollerSectionInfo.fastScrollToItem = appItem;
+                // Create an app item
+                AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++);
+                if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
+                    lastFastScrollerSectionInfo.fastScrollToItem = appItem;
+                }
+                mAdapterItems.add(appItem);
+                mFilteredApps.add(info);
             }
-            mAdapterItems.add(appItem);
-            mFilteredApps.add(info);
+        } else {
+            mAdapterItems.addAll(mSearchResults);
+            List<AppInfo> appInfos = mSearchResults.stream().filter(
+                    i -> AllAppsGridAdapter.isIconViewType(i.viewType)).map(i -> i.appInfo).collect(
+                    Collectors.toList());
+            mFilteredApps.addAll(appInfos);
+            if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+                // Append the search market item
+                if (hasNoFilteredResults()) {
+                    mAdapterItems.add(AdapterItem.asEmptySearch(position++));
+                } else {
+                    mAdapterItems.add(AdapterItem.asAllAppsDivider(position++));
+                }
+                mAdapterItems.add(AdapterItem.asMarketSearch(position++));
+
+            }
         }
-
-        if (hasFilter()) {
-            // Append the search market item
-            if (hasNoFilteredResults()) {
-                mAdapterItems.add(AdapterItem.asEmptySearch(position++));
-            } else {
-                mAdapterItems.add(AdapterItem.asAllAppsDivider(position++));
-            }
-            mAdapterItems.add(AdapterItem.asMarketSearch(position++));
-        }
-
         if (mNumAppsPerRow != 0) {
             // Update the number of rows in the adapter after we do all the merging (otherwise, we
             // would have to shift the values again)
@@ -381,18 +403,4 @@
             }
         }
     }
-
-    private List<AppInfo> getFiltersAppInfos() {
-        if (mSearchResults == null) {
-            return mApps;
-        }
-        ArrayList<AppInfo> result = new ArrayList<>();
-        for (ComponentKey key : mSearchResults) {
-            AppInfo match = mAllAppsStore.getApp(key);
-            if (match != null) {
-                result.add(match);
-            }
-        }
-        return result;
-    }
 }
diff --git a/src/com/android/launcher3/allapps/SearchUiManager.java b/src/com/android/launcher3/allapps/SearchUiManager.java
index 7d5363f..aa056a0 100644
--- a/src/com/android/launcher3/allapps/SearchUiManager.java
+++ b/src/com/android/launcher3/allapps/SearchUiManager.java
@@ -66,12 +66,8 @@
     }
 
     /**
-     * Called to control how the search UI result should be handled.
-     *
-     * @param isEnabled when {@code true}, the search is all handled inside AOSP
-     *                  and is not overlayable.
-     * @return the searchbox edit text object
+     * @return the edit text object
      */
     @Nullable
-    EditText setTextSearchEnabled(boolean isEnabled);
+    EditText getEditText();
 }
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index df1cd26..db94e8b 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -28,7 +28,7 @@
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.ExtendedEditText;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.allapps.AlphabeticalAppsList;
 import com.android.launcher3.util.PackageManagerHelper;
 
 import java.util.ArrayList;
@@ -50,6 +50,7 @@
     public void setVisibility(int visibility) {
         mInput.setVisibility(visibility);
     }
+
     /**
      * Sets the references to the apps model and the search result callback.
      */
@@ -164,9 +165,9 @@
         /**
          * Called when the search is complete.
          *
-         * @param apps sorted list of matching components or null if in case of failure.
+         * @param items sorted list of search result adapter items.
          */
-        void onSearchResult(String query, ArrayList<ComponentKey> apps);
+        void onSearchResult(String query, ArrayList<AlphabeticalAppsList.AdapterItem> items);
 
         /**
          * Called when the search results should be cleared.
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
index 356c52c..16a1efd 100644
--- a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
+++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
@@ -38,13 +38,13 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.ExtendedEditText;
 import com.android.launcher3.Insettable;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.allapps.AlphabeticalAppsList;
 import com.android.launcher3.allapps.SearchUiManager;
 import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.util.ComponentKey;
 
 import java.util.ArrayList;
 
@@ -135,7 +135,8 @@
         mApps = appsView.getApps();
         mAppsView = appsView;
         mSearchBarController.initialize(
-                new DefaultAppSearchAlgorithm(mApps.getApps()), this, mLauncher, this);
+                new DefaultAppSearchAlgorithm(LauncherAppState.getInstance(mLauncher)), this,
+                mLauncher, this);
     }
 
     @Override
@@ -168,9 +169,9 @@
     }
 
     @Override
-    public void onSearchResult(String query, ArrayList<ComponentKey> apps) {
-        if (apps != null) {
-            mApps.setOrderedFilter(apps);
+    public void onSearchResult(String query, ArrayList<AlphabeticalAppsList.AdapterItem> items) {
+        if (items != null) {
+            mApps.setSearchResults(items);
             notifyResultChanged();
             mAppsView.setLastSearchQuery(query);
         }
@@ -178,7 +179,7 @@
 
     @Override
     public void clearSearchResult() {
-        if (mApps.setOrderedFilter(null)) {
+        if (mApps.setSearchResults(null)) {
             notifyResultChanged();
         }
 
@@ -216,7 +217,7 @@
     }
 
     @Override
-    public EditText setTextSearchEnabled(boolean isEnabled) {
+    public EditText getEditText() {
         return this;
     }
 }
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java b/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java
new file mode 100644
index 0000000..5beb956
--- /dev/null
+++ b/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java
@@ -0,0 +1,92 @@
+/*
+ * 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.allapps.search;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
+import com.android.launcher3.model.AllAppsList;
+import com.android.launcher3.model.BaseModelUpdateTask;
+import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.data.AppInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * A device search section for handling app searches
+ */
+public class AppsSearchPipeline implements SearchPipeline {
+
+    private static final int MAX_RESULTS_COUNT = 5;
+
+    private final SearchSectionInfo mSearchSectionInfo;
+    private final LauncherAppState mLauncherAppState;
+
+    public AppsSearchPipeline(LauncherAppState launcherAppState) {
+        mLauncherAppState = launcherAppState;
+        mSearchSectionInfo = new SearchSectionInfo(R.string.search_corpus_apps);
+    }
+
+    @Override
+    @WorkerThread
+    public void performSearch(String query, Consumer<ArrayList<AdapterItem>> callback) {
+        mLauncherAppState.getModel().enqueueModelUpdateTask(new BaseModelUpdateTask() {
+            @Override
+            public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
+                callback.accept(getAdapterItems(getTitleMatchResult(apps.data, query)));
+            }
+        });
+    }
+
+    /**
+     * Filters {@link AppInfo}s matching specified query
+     */
+    public static ArrayList<AppInfo> getTitleMatchResult(List<AppInfo> apps, String query) {
+        // Do an intersection of the words in the query and each title, and filter out all the
+        // apps that don't match all of the words in the query.
+        final String queryTextLower = query.toLowerCase();
+        final ArrayList<AppInfo> result = new ArrayList<>();
+        DefaultAppSearchAlgorithm.StringMatcher matcher =
+                DefaultAppSearchAlgorithm.StringMatcher.getInstance();
+        for (AppInfo info : apps) {
+            if (DefaultAppSearchAlgorithm.matches(info, queryTextLower, matcher)) {
+                result.add(info);
+            }
+        }
+        return result;
+    }
+
+    private ArrayList<AdapterItem> getAdapterItems(List<AppInfo> matchingApps) {
+        ArrayList<AdapterItem> items = new ArrayList<>();
+        if (matchingApps.isEmpty()) {
+            return items;
+        }
+        items.add(AdapterItem.asSearchTitle(mSearchSectionInfo, 0));
+        int existingItems = items.size();
+        int searchResultsCount = Math.min(matchingApps.size(), MAX_RESULTS_COUNT);
+        for (int i = 0; i < searchResultsCount; i++) {
+            AdapterItem appItem = AdapterItem.asApp(i + existingItems, "", matchingApps.get(i), i);
+            appItem.searchSectionInfo = mSearchSectionInfo;
+            items.add(appItem);
+        }
+
+        return items;
+    }
+}
diff --git a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
index f72a988..db10311 100644
--- a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
+++ b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
@@ -17,24 +17,22 @@
 
 import android.os.Handler;
 
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.util.ComponentKey;
 
 import java.text.Collator;
-import java.util.ArrayList;
-import java.util.List;
 
 /**
  * The default search implementation.
  */
 public class DefaultAppSearchAlgorithm implements SearchAlgorithm {
 
-    private final List<AppInfo> mApps;
     protected final Handler mResultHandler;
+    private final AppsSearchPipeline mAppsSearchPipeline;
 
-    public DefaultAppSearchAlgorithm(List<AppInfo> apps) {
-        mApps = apps;
+    public DefaultAppSearchAlgorithm(LauncherAppState launcherAppState) {
         mResultHandler = new Handler();
+        mAppsSearchPipeline = new AppsSearchPipeline(launcherAppState);
     }
 
     @Override
@@ -47,28 +45,8 @@
     @Override
     public void doSearch(final String query,
             final AllAppsSearchBarController.Callbacks callback) {
-        final ArrayList<ComponentKey> result = getTitleMatchResult(query);
-        mResultHandler.post(new Runnable() {
-
-            @Override
-            public void run() {
-                callback.onSearchResult(query, result);
-            }
-        });
-    }
-
-    private ArrayList<ComponentKey> getTitleMatchResult(String query) {
-        // Do an intersection of the words in the query and each title, and filter out all the
-        // apps that don't match all of the words in the query.
-        final String queryTextLower = query.toLowerCase();
-        final ArrayList<ComponentKey> result = new ArrayList<>();
-        StringMatcher matcher = StringMatcher.getInstance();
-        for (AppInfo info : mApps) {
-            if (matches(info, queryTextLower, matcher)) {
-                result.add(info.toComponentKey());
-            }
-        }
-        return result;
+        mAppsSearchPipeline.performSearch(query,
+                results -> mResultHandler.post(() -> callback.onSearchResult(query, results)));
     }
 
     public static boolean matches(AppInfo info, String query, StringMatcher matcher) {
diff --git a/src/com/android/launcher3/allapps/search/SearchPipeline.java b/src/com/android/launcher3/allapps/search/SearchPipeline.java
new file mode 100644
index 0000000..3216740
--- /dev/null
+++ b/src/com/android/launcher3/allapps/search/SearchPipeline.java
@@ -0,0 +1,32 @@
+/*
+ * 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.allapps.search;
+
+import com.android.launcher3.allapps.AlphabeticalAppsList;
+
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+/**
+ * An interface for handling search within pipeline
+ */
+public interface SearchPipeline {
+
+    /**
+     * Perform query
+     */
+    void performSearch(String query, Consumer<ArrayList<AlphabeticalAppsList.AdapterItem>> cb);
+}
diff --git a/src/com/android/launcher3/allapps/search/SearchSectionInfo.java b/src/com/android/launcher3/allapps/search/SearchSectionInfo.java
new file mode 100644
index 0000000..880b246
--- /dev/null
+++ b/src/com/android/launcher3/allapps/search/SearchSectionInfo.java
@@ -0,0 +1,36 @@
+/*
+ * 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.allapps.search;
+
+import android.content.Context;
+
+/**
+ * Info class for a search section
+ */
+public class SearchSectionInfo {
+    private final int mTitleResId;
+
+    public SearchSectionInfo(int titleResId) {
+        mTitleResId = titleResId;
+    }
+
+    /**
+     * Returns the section's title
+     */
+    public String getTitle(Context context) {
+        return context.getString(mTitleResId);
+    }
+}
diff --git a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
index d86bb17..30c3417 100644
--- a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
+++ b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
@@ -70,36 +70,33 @@
         final Bundle parcel = new Bundle();
         parcel.putInt(TestProtocol.STATE_FIELD, stateOrdinal);
 
-        sendEventToTest(accessibilityManager, TestProtocol.SWITCHED_TO_STATE_MESSAGE, parcel);
+        sendEventToTest(
+                accessibilityManager, context, TestProtocol.SWITCHED_TO_STATE_MESSAGE, parcel);
         Log.d(TestProtocol.PERMANENT_DIAG_TAG, "sendStateEventToTest: " + stateOrdinal);
     }
 
     public static void sendScrollFinishedEventToTest(Context context) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "sendScrollFinishedEventToTest");
-        }
         final AccessibilityManager accessibilityManager = getAccessibilityManagerForTest(context);
         if (accessibilityManager == null) return;
 
-        sendEventToTest(accessibilityManager, TestProtocol.SCROLL_FINISHED_MESSAGE, null);
+        sendEventToTest(accessibilityManager, context, TestProtocol.SCROLL_FINISHED_MESSAGE, null);
     }
 
     public static void sendPauseDetectedEventToTest(Context context) {
         final AccessibilityManager accessibilityManager = getAccessibilityManagerForTest(context);
         if (accessibilityManager == null) return;
 
-        sendEventToTest(accessibilityManager, TestProtocol.PAUSE_DETECTED_MESSAGE, null);
+        sendEventToTest(accessibilityManager, context, TestProtocol.PAUSE_DETECTED_MESSAGE, null);
     }
 
     private static void sendEventToTest(
-            AccessibilityManager accessibilityManager, String eventTag, Bundle data) {
+            AccessibilityManager accessibilityManager,
+            Context context, String eventTag, Bundle data) {
         final AccessibilityEvent e = AccessibilityEvent.obtain(
                 AccessibilityEvent.TYPE_ANNOUNCEMENT);
         e.setClassName(eventTag);
         e.setParcelableData(data);
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "sendEventToTest " + e);
-        }
+        e.setPackageName(context.getApplicationContext().getPackageName());
         accessibilityManager.sendAccessibilityEvent(e);
     }
 
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 5c4a492..0ab74af 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -94,6 +94,10 @@
     public static final BooleanFlag ENABLE_SUGGESTED_ACTIONS_OVERVIEW = new DeviceFlag(
             "ENABLE_SUGGESTED_ACTIONS_OVERVIEW", false, "Show chip hints on the overview screen");
 
+
+    public static final BooleanFlag ENABLE_DEVICE_SEARCH = getDebugFlag(
+            "ENABLE_DEVICE_SEARCH", false, "Allows on device search in all apps");
+
     public static final BooleanFlag FOLDER_NAME_SUGGEST = new DeviceFlag(
             "FOLDER_NAME_SUGGEST", true,
             "Suggests folder names instead of blank text.");
@@ -177,6 +181,10 @@
     public static final BooleanFlag USER_EVENT_DISPATCHER = new DeviceFlag(
             "USER_EVENT_DISPATCHER", true, "User event dispatcher collects logs.");
 
+    public static final BooleanFlag ENABLE_MINIMAL_DEVICE = new DeviceFlag(
+            "ENABLE_MINIMAL_DEVICE", false,
+            "Allow user to toggle minimal device mode in launcher.");
+
     public static void initialize(Context context) {
         synchronized (sDebugFlags) {
             for (DebugFlag flag : sDebugFlags) {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 1c18402..b91d1c3 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -741,7 +741,8 @@
 
     @Override
     protected View getAccessibilityInitialFocusView() {
-        return mContent.getFirstItem();
+        View firstItem = mContent.getFirstItem();
+        return firstItem != null ? firstItem : super.getAccessibilityInitialFocusView();
     }
 
     private void closeComplete(boolean wasAnimated) {
@@ -1134,6 +1135,9 @@
      * Rearranges the children based on their rank.
      */
     public void rearrangeChildren() {
+        if (!mContent.areViewsBound()) {
+            return;
+        }
         mContent.arrangeChildren(getIconsInReadingOrder());
         mItemsInvalidated = true;
     }
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 75275b2..32d061c 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -129,6 +129,8 @@
     private float mDotScale;
     private Animator mDotScaleAnim;
 
+    private Rect mTouchArea = new Rect();
+
     private final PointF mTranslationForReorderBounce = new PointF(0, 0);
     private final PointF mTranslationForReorderPreview = new PointF(0, 0);
     private float mScaleForReorderBounce = 1f;
@@ -711,6 +713,11 @@
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
+        if (event.getAction() == MotionEvent.ACTION_DOWN
+                && shouldIgnoreTouchDown(event.getX(), event.getY())) {
+            return false;
+        }
+
         // Call the superclass onTouchEvent first, because sometimes it changes the state to
         // isPressed() on an ACTION_UP
         super.onTouchEvent(event);
@@ -719,6 +726,15 @@
         return true;
     }
 
+    /**
+     * Returns true if the touch down at the provided position be ignored
+     */
+    protected boolean shouldIgnoreTouchDown(float x, float y) {
+        mTouchArea.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
+                getHeight() - getPaddingBottom());
+        return !mTouchArea.contains((int) x, (int) y);
+    }
+
     @Override
     public void cancelLongPress() {
         super.cancelLongPress();
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 32531c0..a08dd30 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -500,6 +500,9 @@
      * Reorders the items such that the {@param empty} spot moves to {@param target}
      */
     public void realTimeReorder(int empty, int target) {
+        if (!mViewsBound) {
+            return;
+        }
         completePendingPageChanges();
         int delay = 0;
         float delayAmount = START_VIEW_REORDER_DELAY;
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 458ffa3..d8eb838 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -74,6 +74,7 @@
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.LoaderResults;
 import com.android.launcher3.model.LoaderTask;
+import com.android.launcher3.model.ModelDelegate;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.model.data.AppInfo;
@@ -126,7 +127,7 @@
      */
     public static class PreviewContext extends ContextWrapper {
 
-        private static final Set<MainThreadInitializedObject> WHITELIST = new HashSet<>(
+        private final Set<MainThreadInitializedObject> mAllowedObjects = new HashSet<>(
                 Arrays.asList(UserCache.INSTANCE, InstallSessionHelper.INSTANCE,
                         LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE,
                         CustomWidgetManager.INSTANCE, PluginManagerWrapper.INSTANCE));
@@ -160,7 +161,7 @@
          */
         public <T> T getObject(MainThreadInitializedObject<T> mainThreadInitializedObject,
                 MainThreadInitializedObject.ObjectProvider<T> provider) {
-            if (!WHITELIST.contains(mainThreadInitializedObject)) {
+            if (!mAllowedObjects.contains(mainThreadInitializedObject)) {
                 throw new IllegalStateException("Leaking unknown objects");
             }
             if (mainThreadInitializedObject == LauncherAppState.INSTANCE) {
@@ -579,7 +580,7 @@
         private final FutureTask<WorkspaceResult> mTask = new FutureTask<>(this);
 
         WorkspaceItemsInfoFromPreviewFetcher(LauncherAppState app) {
-            super(app, null, new BgDataModel(), null);
+            super(app, null, new BgDataModel(), new ModelDelegate(), null);
         }
 
         @Override
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index acf4482..de72534 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -30,11 +30,16 @@
 import com.android.launcher3.userevent.LauncherLogProto;
 import com.android.launcher3.util.ResourceBasedOverride;
 
+import java.util.List;
+
 /**
  * Handles the user event logging in R+.
+ *
+ * <pre>
  * All of the event ids are defined here.
  * Most of the methods are dummy methods for Launcher3
  * Actual call happens only for Launcher variant that implements QuickStep.
+ * </pre>
  */
 public class StatsLogManager implements ResourceBasedOverride {
 
@@ -46,8 +51,8 @@
     public static final int LAUNCHER_STATE_UNCHANGED = 5;
 
     /**
-     * Returns proper launcher state enum for {@link StatsLogManager}
-     * (to be removed during UserEventDispatcher cleanup)
+     * Returns proper launcher state enum for {@link StatsLogManager}(to be removed during
+     * UserEventDispatcher cleanup)
      */
     public static int containerTypeToAtomState(int containerType) {
         switch (containerType) {
@@ -64,9 +69,8 @@
     }
 
     /**
-     * Returns event enum based on the two {@link ContainerType} transition information when
-     * swipe gesture happens.
-     * (to be removed during UserEventDispatcher cleanup)
+     * Returns event enum based on the two {@link ContainerType} transition information when swipe
+     * gesture happens(to be removed during UserEventDispatcher cleanup).
      */
     public static EventEnum getLauncherAtomEvent(int startContainerType,
             int targetContainerType, EventEnum fallbackEvent) {
@@ -270,7 +274,46 @@
         LAUNCHER_SELECT_MODE_CLOSE(583),
 
         @UiEvent(doc = "User tapped on the highlight items in select mode")
-        LAUNCHER_SELECT_MODE_ITEM(584);
+        LAUNCHER_SELECT_MODE_ITEM(584),
+
+        @UiEvent(doc = "Notification dot on app icon enabled.")
+        LAUNCHER_NOTIFICATION_DOT_ENABLED(611),
+
+        @UiEvent(doc = "Notification dot on app icon disabled.")
+        LAUNCHER_NOTIFICATION_DOT_DISABLED(612),
+
+        @UiEvent(doc = "For new apps, add app icons to home screen enabled.")
+        LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_ENABLED(613),
+
+        @UiEvent(doc = "For new apps, add app icons to home screen disabled.")
+        LAUNCHER_ADD_NEW_APPS_TO_HOME_SCREEN_DISABLED(614),
+
+        @UiEvent(doc = "Home screen rotation is enabled when phone is rotated.")
+        LAUNCHER_HOME_SCREEN_ROTATION_ENABLED(615),
+
+        @UiEvent(doc = "Home screen rotation is disabled when phone is rotated.")
+        LAUNCHER_HOME_SCREEN_ROTATION_DISABLED(616),
+
+        @UiEvent(doc = "Suggestions in all apps list enabled.")
+        LAUNCHER_ALL_APPS_SUGGESTIONS_ENABLED(619),
+
+        @UiEvent(doc = "Suggestions in all apps list disabled.")
+        LAUNCHER_ALL_APPS_SUGGESTIONS_DISABLED(620),
+
+        @UiEvent(doc = "Suggestions on home screen is enabled.")
+        LAUNCHER_HOME_SCREEN_SUGGESTIONS_ENABLED(621),
+
+        @UiEvent(doc = "Suggestions on home screen is disabled.")
+        LAUNCHER_HOME_SCREEN_SUGGESTIONS_DISABLED(622),
+
+        @UiEvent(doc = "System navigation is 3 button mode.")
+        LAUNCHER_NAVIGATION_MODE_3_BUTTON(623),
+
+        @UiEvent(doc = "System navigation mode is 2 button mode.")
+        LAUNCHER_NAVIGATION_MODE_2_BUTTON(624),
+
+        @UiEvent(doc = "System navigation mode is 0 button mode/gesture navigation mode .")
+        LAUNCHER_NAVIGATION_MODE_GESTURE_BUTTON(625);
 
         // ADD MORE
 
@@ -401,8 +444,8 @@
     }
 
     /**
-     * Logs snapshot, or impression of the current workspace.
+     * Logs impression of the current workspace with additional launcher events.
      */
-    public void logSnapshot() {
+    public void logSnapshot(List<EventEnum> additionalEvents) {
     }
 }
diff --git a/src/com/android/launcher3/model/AppLaunchTracker.java b/src/com/android/launcher3/model/AppLaunchTracker.java
deleted file mode 100644
index a93c0dd..0000000
--- a/src/com/android/launcher3/model/AppLaunchTracker.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.model;
-
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
-
-import com.android.launcher3.R;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-/**
- * Callback for receiving various app launch events
- */
-public class AppLaunchTracker implements ResourceBasedOverride {
-
-    public static final MainThreadInitializedObject<AppLaunchTracker> INSTANCE =
-            forOverride(AppLaunchTracker.class, R.string.app_launch_tracker_class);
-
-    public void onReturnedToHome() { }
-}
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index 9013cba..d1e5017 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -22,7 +22,9 @@
 import com.android.launcher3.LauncherModel.CallbackTask;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
 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.WorkspaceItemInfo;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ItemInfoMatcher;
@@ -30,7 +32,10 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
 
 /**
  * Extension of {@link ModelUpdateTask} with some utility methods
@@ -88,11 +93,27 @@
         return mModel.getWriter(false /* hasVerticalHotseat */, false /* verifyChanges */);
     }
 
-
-    public void bindUpdatedWorkspaceItems(final ArrayList<WorkspaceItemInfo> updatedShortcuts) {
-        if (!updatedShortcuts.isEmpty()) {
-            scheduleCallbackTask(c -> c.bindWorkspaceItemsChanged(updatedShortcuts));
+    public void bindUpdatedWorkspaceItems(List<WorkspaceItemInfo> allUpdates) {
+        // Bind workspace items
+        List<WorkspaceItemInfo> workspaceUpdates = allUpdates.stream()
+                .filter(info -> info.id != ItemInfo.NO_ID)
+                .collect(Collectors.toList());
+        if (!workspaceUpdates.isEmpty()) {
+            scheduleCallbackTask(c -> c.bindWorkspaceItemsChanged(workspaceUpdates));
         }
+
+        // Bind extra items if any
+        allUpdates.stream()
+                .mapToInt(info -> info.container)
+                .distinct()
+                .mapToObj(mDataModel.extraItems::get)
+                .filter(Objects::nonNull)
+                .forEach(this::bindExtraContainerItems);
+    }
+
+    public void bindExtraContainerItems(FixedContainerItems item) {
+        FixedContainerItems copy = item.clone();
+        scheduleCallbackTask(c -> c.bindExtraContainerItems(copy));
     }
 
     public void bindDeepShortcuts(BgDataModel dataModel) {
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 7524920..fd8520d 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -15,19 +15,25 @@
  */
 package com.android.launcher3.model;
 
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_KEY_FIELDS_ONLY;
+
 import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
 import static com.android.launcher3.shortcuts.ShortcutRequest.PINNED;
 
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.mapping;
+
 import android.content.Context;
 import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.Log;
-import android.util.MutableInt;
 
 import com.android.launcher3.InstallShortcutReceiver;
 import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.AppInfo;
@@ -36,8 +42,10 @@
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.PromiseAppInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
@@ -50,14 +58,16 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.function.BiConsumer;
+import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * All the data stored in-memory and managed by the LauncherModel
@@ -89,9 +99,9 @@
     public final IntSparseArrayMap<FolderInfo> folders = new IntSparseArrayMap<>();
 
     /**
-     * Map of ShortcutKey to the number of times it is pinned.
+     * Extra container based items
      */
-    public final Map<ShortcutKey, MutableInt> pinnedShortcutCounts = new HashMap<>();
+    public final IntSparseArrayMap<FixedContainerItems> extraItems = new IntSparseArrayMap<>();
 
     /**
      * List of all cached predicted items visible on home screen
@@ -99,13 +109,6 @@
     public final ArrayList<AppInfo> cachedPredictedItems = new ArrayList<>();
 
     /**
-     * @see Callbacks#FLAG_HAS_SHORTCUT_PERMISSION
-     * @see Callbacks#FLAG_QUIET_MODE_ENABLED
-     * @see Callbacks#FLAG_QUIET_MODE_CHANGE_PERMISSION
-     */
-    public int flags;
-
-    /**
      * Maps all launcher activities to counts of their shortcuts.
      */
     public final HashMap<ComponentKey, Integer> deepShortcutMap = new HashMap<>();
@@ -128,8 +131,8 @@
         appWidgets.clear();
         folders.clear();
         itemsIdMap.clear();
-        pinnedShortcutCounts.clear();
         deepShortcutMap.clear();
+        extraItems.clear();
     }
 
     /**
@@ -182,6 +185,7 @@
     }
 
     public synchronized void removeItem(Context context, Iterable<? extends ItemInfo> items) {
+        ArraySet<UserHandle> updatedDeepShortcuts = new ArraySet<>();
         for (ItemInfo item : items) {
             switch (item.itemType) {
                 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
@@ -200,14 +204,7 @@
                     workspaceItems.remove(item);
                     break;
                 case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
-                    // Decrement pinned shortcut count
-                    ShortcutKey pinnedShortcut = ShortcutKey.fromItemInfo(item);
-                    MutableInt count = pinnedShortcutCounts.get(pinnedShortcut);
-                    if ((count == null || --count.value == 0)
-                            && !InstallShortcutReceiver.getPendingShortcuts(context)
-                                .contains(pinnedShortcut)) {
-                        unpinShortcut(context, pinnedShortcut);
-                    }
+                    updatedDeepShortcuts.add(item.user);
                     // Fall through.
                 }
                 case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
@@ -221,6 +218,7 @@
             }
             itemsIdMap.remove(item.id);
         }
+        updatedDeepShortcuts.forEach(user -> updateShortcutPinnedState(context, user));
     }
 
     public synchronized void addItem(Context context, ItemInfo item, boolean newItem) {
@@ -230,23 +228,7 @@
                 folders.put(item.id, (FolderInfo) item);
                 workspaceItems.add(item);
                 break;
-            case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
-                // Increment the count for the given shortcut
-                ShortcutKey pinnedShortcut = ShortcutKey.fromItemInfo(item);
-                MutableInt count = pinnedShortcutCounts.get(pinnedShortcut);
-                if (count == null) {
-                    count = new MutableInt(1);
-                    pinnedShortcutCounts.put(pinnedShortcut, count);
-                } else {
-                    count.value++;
-                }
-
-                // Since this is a new item, pin the shortcut in the system server.
-                if (newItem && count.value == 1) {
-                    updatePinnedShortcuts(context, pinnedShortcut, List::add);
-                }
-                // Fall through
-            }
+            case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
             case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
             case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                 if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
@@ -271,36 +253,87 @@
                 appWidgets.add((LauncherAppWidgetInfo) item);
                 break;
         }
+        if (newItem && item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+            updateShortcutPinnedState(context, item.user);
+        }
     }
 
     /**
-     * Removes the given shortcut from the current list of pinned shortcuts.
-     * (Runs on background thread)
+     * Updates the deep shortucts state in system to match out internal model, pinning any missing
+     * shortcuts and unpinning any extra shortcuts.
      */
-    public void unpinShortcut(Context context, ShortcutKey key) {
-        updatePinnedShortcuts(context, key, List::remove);
+    public void updateShortcutPinnedState(Context context) {
+        for (UserHandle user : UserCache.INSTANCE.get(context).getUserProfiles()) {
+            updateShortcutPinnedState(context, user);
+        }
     }
 
-    private void updatePinnedShortcuts(Context context, ShortcutKey key,
-            BiConsumer<List<String>, String> idOp) {
+    /**
+     * Updates the deep shortucts state in system to match out internal model, pinning any missing
+     * shortcuts and unpinning any extra shortcuts.
+     */
+    public synchronized void updateShortcutPinnedState(Context context, UserHandle user) {
         if (GO_DISABLE_WIDGETS) {
             return;
         }
-        String packageName = key.componentName.getPackageName();
-        String id = key.getId();
-        UserHandle user = key.user;
-        List<String> pinnedIds = new ShortcutRequest(context, user)
-                .forPackage(packageName)
-                .query(PINNED)
-                .stream()
-                .map(ShortcutInfo::getId)
-                .collect(Collectors.toCollection(ArrayList::new));
-        idOp.accept(pinnedIds, id);
-        try {
-            context.getSystemService(LauncherApps.class).pinShortcuts(packageName, pinnedIds, user);
-        } catch (SecurityException | IllegalStateException e) {
-            Log.w(TAG, "Failed to pin shortcut", e);
+
+        // Collect all system shortcuts
+        QueryResult result = new ShortcutRequest(context, user)
+                .query(PINNED | FLAG_GET_KEY_FIELDS_ONLY);
+        if (!result.wasSuccess()) {
+            return;
         }
+        // Map of packageName to shortcutIds that are currently in the system
+        Map<String, Set<String>> systemMap = result.stream()
+                .collect(groupingBy(ShortcutInfo::getPackage,
+                        mapping(ShortcutInfo::getId, Collectors.toSet())));
+
+        // Collect all model shortcuts
+        Stream.Builder<WorkspaceItemInfo> itemStream = Stream.builder();
+        forAllWorkspaceItemInfos(user, itemStream::accept);
+        // Map of packageName to shortcutIds that are currently in our model
+        Map<String, Set<String>> modelMap = Stream.concat(
+                    // Model shortcuts
+                    itemStream.build()
+                        .filter(wi -> wi.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT)
+                        .map(ShortcutKey::fromItemInfo),
+                    // Pending shortcuts
+                    InstallShortcutReceiver.getPendingShortcuts(context)
+                        .stream().filter(si -> si.user.equals(user)))
+                .collect(groupingBy(ShortcutKey::getPackageName,
+                        mapping(ShortcutKey::getId, Collectors.toSet())));
+
+        // Check for diff
+        for (Map.Entry<String, Set<String>> entry : modelMap.entrySet()) {
+            Set<String> modelShortcuts = entry.getValue();
+            Set<String> systemShortcuts = systemMap.remove(entry.getKey());
+            if (systemShortcuts == null) {
+                systemShortcuts = Collections.emptySet();
+            }
+
+            // Do not use .equals as it can vary based on the type of set
+            if (systemShortcuts.size() != modelShortcuts.size()
+                    || !systemShortcuts.containsAll(modelShortcuts)) {
+                // Update system state for this package
+                try {
+                    context.getSystemService(LauncherApps.class).pinShortcuts(
+                            entry.getKey(), new ArrayList<>(modelShortcuts), user);
+                } catch (SecurityException | IllegalStateException e) {
+                    Log.w(TAG, "Failed to pin shortcut", e);
+                }
+            }
+        }
+
+        // If there are any extra pinned shortcuts, remove them
+        systemMap.keySet().forEach(packageName -> {
+            // Update system state
+            try {
+                context.getSystemService(LauncherApps.class).pinShortcuts(
+                        packageName, Collections.emptyList(), user);
+            } catch (SecurityException | IllegalStateException e) {
+                Log.w(TAG, "Failed to unpin shortcut", e);
+            }
+        });
     }
 
     /**
@@ -360,8 +393,40 @@
                 op.accept((WorkspaceItemInfo) info);
             }
         }
+
+        for (int i = extraItems.size() - 1; i >= 0; i--) {
+            for (ItemInfo info : extraItems.valueAt(i).items) {
+                if (info instanceof WorkspaceItemInfo && userHandle.equals(info.user)) {
+                    op.accept((WorkspaceItemInfo) info);
+                }
+            }
+        }
     }
 
+    /**
+     * An object containing items corresponding to a fixed container
+     */
+    public static class FixedContainerItems {
+
+        public final int containerId;
+        public final List<ItemInfo> items;
+
+        public FixedContainerItems(int containerId) {
+            this(containerId, new ArrayList<>());
+        }
+
+        public FixedContainerItems(int containerId, List<ItemInfo> items) {
+            this.containerId = containerId;
+            this.items = items;
+        }
+
+        @Override
+        public FixedContainerItems clone() {
+            return new FixedContainerItems(containerId, new ArrayList<>(items));
+        }
+    }
+
+
     public interface Callbacks {
         // If the launcher has permission to access deep shortcuts.
         int FLAG_HAS_SHORTCUT_PERMISSION = 1 << 0;
@@ -384,7 +449,7 @@
         void bindAppsAdded(IntArray newScreens,
                 ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated);
         void bindPromiseAppProgressUpdated(PromiseAppInfo app);
-        void bindWorkspaceItemsChanged(ArrayList<WorkspaceItemInfo> updated);
+        void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated);
         void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets);
         void bindRestoreItemsChange(HashSet<ItemInfo> updates);
         void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher);
@@ -393,6 +458,11 @@
         void executeOnNextDraw(ViewOnDrawExecutor executor);
         void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap);
 
+        /**
+         * Binds extra item provided any external source
+         */
+        default void bindExtraContainerItems(FixedContainerItems item) { }
+
         void bindAllApplications(AppInfo[] apps, int flags);
 
         /**
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 4a64522..b067909 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -46,12 +46,10 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.LongSparseArray;
-import android.util.MutableInt;
 import android.util.TimingLogger;
 
 import androidx.annotation.WorkerThread;
 
-import com.android.launcher3.InstallShortcutReceiver;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
@@ -94,7 +92,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CancellationException;
@@ -112,6 +109,7 @@
     protected final LauncherAppState mApp;
     private final AllAppsList mBgAllAppsList;
     protected final BgDataModel mBgDataModel;
+    private final ModelDelegate mModelDelegate;
 
     private FirstScreenBroadcast mFirstScreenBroadcast;
 
@@ -131,10 +129,11 @@
     private boolean mStopped;
 
     public LoaderTask(LauncherAppState app, AllAppsList bgAllAppsList, BgDataModel dataModel,
-            LoaderResults results) {
+            ModelDelegate modelDelegate, LoaderResults results) {
         mApp = app;
         mBgAllAppsList = bgAllAppsList;
         mBgDataModel = dataModel;
+        mModelDelegate = modelDelegate;
         mResults = results;
 
         mLauncherApps = mApp.getContext().getSystemService(LauncherApps.class);
@@ -770,6 +769,9 @@
                 IOUtils.closeSilently(c);
             }
 
+            // Load delegate items
+            mModelDelegate.loadItems();
+
             // Break early if we've stopped loading
             if (mStopped) {
                 mBgDataModel.clear();
@@ -794,17 +796,8 @@
                         LauncherSettings.Settings.METHOD_REMOVE_GHOST_WIDGETS);
             }
 
-            // Unpin shortcuts that don't exist on the workspace.
-            HashSet<ShortcutKey> pendingShortcuts =
-                    InstallShortcutReceiver.getPendingShortcuts(context);
-            for (ShortcutKey key : shortcutKeyToPinnedShortcuts.keySet()) {
-                MutableInt numTimesPinned = mBgDataModel.pinnedShortcutCounts.get(key);
-                if ((numTimesPinned == null || numTimesPinned.value == 0)
-                        && !pendingShortcuts.contains(key)) {
-                    // Shortcut is pinned but doesn't exist on the workspace; unpin it.
-                    mBgDataModel.unpinShortcut(context, key);
-                }
-            }
+            // Update pinned state of model shortcuts
+            mBgDataModel.updateShortcutPinnedState(context);
 
             // Sort the folder items, update ranks, and make sure all preview items are high res.
             FolderGridOrganizer verifier =
diff --git a/src/com/android/launcher3/model/ModelDelegate.java b/src/com/android/launcher3/model/ModelDelegate.java
new file mode 100644
index 0000000..ce4eed5
--- /dev/null
+++ b/src/com/android/launcher3/model/ModelDelegate.java
@@ -0,0 +1,75 @@
+/*
+ * 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.model;
+
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
+
+import android.content.Context;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.util.ResourceBasedOverride;
+
+/**
+ * Class to extend LauncherModel functionality to provide extra data
+ */
+public class ModelDelegate implements ResourceBasedOverride {
+
+    /**
+     * Creates and initializes a new instance of the delegate
+     */
+    public static ModelDelegate newInstance(
+            Context context, LauncherAppState app, AllAppsList appsList, BgDataModel dataModel) {
+        ModelDelegate delegate = Overrides.getObject(
+                ModelDelegate.class, context, R.string.model_delegate_class);
+
+        delegate.mApp = app;
+        delegate.mAppsList = appsList;
+        delegate.mDataModel = dataModel;
+        return delegate;
+    }
+
+    protected LauncherAppState mApp;
+    protected AllAppsList mAppsList;
+    protected BgDataModel mDataModel;
+
+    public ModelDelegate() { }
+
+    /**
+     * Called periodically to validate and update any data
+     */
+    @WorkerThread
+    public void validateData() {
+        if (hasShortcutsPermission(mApp.getContext())
+                != mAppsList.hasShortcutHostPermission()) {
+            mApp.getModel().forceReload();
+        }
+    }
+
+    /**
+     * Load delegate items if any in the data model
+     */
+    @WorkerThread
+    public void loadItems() { }
+
+    /**
+     * Called when the delegate is no loner needed
+     */
+    @WorkerThread
+    public void destroy() { }
+}
diff --git a/src/com/android/launcher3/model/PredictionModel.java b/src/com/android/launcher3/model/PredictionModel.java
index 1429843..cb3903d 100644
--- a/src/com/android/launcher3/model/PredictionModel.java
+++ b/src/com/android/launcher3/model/PredictionModel.java
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 package com.android.launcher3.model;
+
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.content.ComponentName;
@@ -24,6 +25,7 @@
 import androidx.annotation.AnyThread;
 import androidx.annotation.WorkerThread;
 
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.pm.UserCache;
@@ -64,18 +66,28 @@
         mUserCache = UserCache.INSTANCE.get(mContext);
 
     }
+
     /**
      * Formats and stores a list of component key in device preferences.
      */
     @AnyThread
     public void cachePredictionComponentKeys(List<ComponentKey> componentKeys) {
         MODEL_EXECUTOR.execute(() -> {
+            LauncherAppState appState = LauncherAppState.getInstance(mContext);
             StringBuilder builder = new StringBuilder();
             int count = Math.min(componentKeys.size(), MAX_CACHE_ITEMS);
             for (int i = 0; i < count; i++) {
                 builder.append(serializeComponentKeyToString(componentKeys.get(i)));
                 builder.append("\n");
             }
+            if (componentKeys.isEmpty() /* should invalidate loader items */) {
+                appState.getModel().enqueueModelUpdateTask(new BaseModelUpdateTask() {
+                    @Override
+                    public void execute(LauncherAppState app, BgDataModel model, AllAppsList apps) {
+                        model.cachedPredictedItems.clear();
+                    }
+                });
+            }
             mDevicePrefs.edit().putString(CACHED_ITEMS_KEY, builder.toString()).apply();
         });
     }
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 5aab41a..2d7d6b0 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -21,8 +21,10 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArrayMap;
+import android.util.Log;
 import android.util.LongSparseArray;
 
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
@@ -102,6 +104,9 @@
                 mUsers = null;
                 mUserToSerialMap = null;
             }
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.WORK_PROFILE_REMOVED, "Work profile removed", new Exception());
+            }
         }
     }
 
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index 8f21d5c..31c3014 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -225,7 +225,7 @@
     }
 
     @Override
-    public void bindWorkspaceItemsChanged(ArrayList<WorkspaceItemInfo> updated) { }
+    public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) { }
 
     @Override
     public void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) { }
diff --git a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
index b12d04f..4baecb7 100644
--- a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
+++ b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
@@ -34,17 +34,23 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.provider.Settings;
+import android.text.Editable;
+import android.text.TextWatcher;
 import android.util.ArrayMap;
 import android.util.Pair;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
+import android.widget.EditText;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceCategory;
 import androidx.preference.PreferenceDataStore;
 import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceGroup;
 import androidx.preference.PreferenceScreen;
 import androidx.preference.PreferenceViewHolder;
 import androidx.preference.SwitchPreference;
@@ -81,6 +87,7 @@
     private PreferenceCategory mPluginsCategory;
     private FlagTogglerPrefUi mFlagTogglerPrefUi;
 
+
     @Override
     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
         IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
@@ -99,6 +106,50 @@
         maybeAddSandboxCategory();
     }
 
+    private void filterPreferences(String query, PreferenceGroup pg) {
+        int count = pg.getPreferenceCount();
+        int hidden = 0;
+        for (int i = 0; i < count; i++) {
+            Preference preference = pg.getPreference(i);
+            if (preference instanceof PreferenceGroup) {
+                filterPreferences(query, (PreferenceGroup) preference);
+            } else {
+                String title = preference.getTitle().toString().toLowerCase().replace("_", " ");
+                if (query.isEmpty() || title.contains(query)) {
+                    preference.setVisible(true);
+                } else {
+                    preference.setVisible(false);
+                    hidden++;
+                }
+            }
+        }
+        pg.setVisible(hidden != count);
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        EditText filterBox = view.findViewById(R.id.filter_box);
+        filterBox.setVisibility(View.VISIBLE);
+        filterBox.addTextChangedListener(new TextWatcher() {
+            @Override
+            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+            }
+
+            @Override
+            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+            }
+
+            @Override
+            public void afterTextChanged(Editable editable) {
+                String query = editable.toString().toLowerCase().replace("_", " ");
+                filterPreferences(query, mPreferenceScreen);
+            }
+        });
+    }
+
     @Override
     public void onDestroy() {
         super.onDestroy();
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java b/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java
index 2daa2fe..eb68592 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java
@@ -23,7 +23,6 @@
 import android.graphics.drawable.Drawable;
 import android.text.TextUtils;
 import android.util.AttributeSet;
-import android.view.MotionEvent;
 import android.widget.Toast;
 
 import com.android.launcher3.BubbleTextView;
@@ -106,12 +105,12 @@
     }
 
     @Override
-    public boolean onTouchEvent(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            // Show toast if user touches the drag handle (long clicks still start the drag).
-            mShowInstructionToast = mDragHandleBounds.contains((int) ev.getX(), (int) ev.getY());
-        }
-        return super.onTouchEvent(ev);
+    protected boolean shouldIgnoreTouchDown(float x, float y) {
+        // Show toast if user touches the drag handle (long clicks still start the drag).
+        mShowInstructionToast = mDragHandleBounds.contains((int) x, (int) y);
+
+        // assume the whole view as clickable
+        return false;
     }
 
     @Override
diff --git a/src/com/android/launcher3/shortcuts/ShortcutKey.java b/src/com/android/launcher3/shortcuts/ShortcutKey.java
index 3ca9490..0c6d675 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutKey.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutKey.java
@@ -30,6 +30,10 @@
         return componentName.getClassName();
     }
 
+    public String getPackageName() {
+        return componentName.getPackageName();
+    }
+
     /**
      * Creates a {@link ShortcutRequest} for this key
      */
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 2644db8..8616881 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -99,6 +99,7 @@
     public static final String REQUEST_DISABLE_DEBUG_TRACING = "disable-debug-tracing";
 
     public static final String REQUEST_OVERVIEW_ACTIONS_ENABLED = "overview-actions-enabled";
+    public static final String REQUEST_OVERVIEW_SHARE_ENABLED = "overview-share-enabled";
 
     public static boolean sDisableSensorRotation;
     public static final String REQUEST_MOCK_SENSOR_ROTATION = "mock-sensor-rotation";
@@ -107,5 +108,5 @@
     public static final String PAUSE_NOT_DETECTED = "b/139891609";
     public static final String OVERIEW_NOT_ALLAPPS = "b/156095088";
     public static final String NO_SWIPE_TO_HOME = "b/158017601";
-    public static final String NO_SCROLL_END_WIDGETS = "b/160238801";
+    public static final String WORK_PROFILE_REMOVED = "b/159671700";
 }
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 3c78b08..66dbf6a 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -37,6 +37,8 @@
 import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
 
+import androidx.core.os.BuildCompat;
+
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.LauncherState;
@@ -44,6 +46,7 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.states.StateAnimationConfig;
@@ -265,8 +268,10 @@
         mCanBlockFling = mFromState == NORMAL;
         mFlingBlockCheck.unblockFling();
         // Must be called after all the animation controllers have been paused
-        if (mToState == ALL_APPS || mToState == NORMAL) {
-            mLauncher.getAllAppsController().onDragStart(mToState == ALL_APPS);
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && BuildCompat.isAtLeastR()
+                && (mToState == ALL_APPS || mToState == NORMAL)) {
+            mLauncher.getAllAppsController().getInsetController().onDragStart(
+                    mToState == ALL_APPS ? 0 : 1);
         }
     }
 
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index c2bfb16..1aaa608 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -72,6 +72,7 @@
         out.halfPageSize = view.getNormalChildHeight() / 2;
         out.halfScreenSize = view.getMeasuredHeight() / 2;
         out.screenCenter = insets.top + view.getPaddingTop() + out.scroll + out.halfPageSize;
+        out.pageParentScale = view.getScaleY();
     }
 
     @Override
@@ -239,7 +240,8 @@
     }
 
     @Override
-    public int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout) {
+    public int getTaskMenuLayoutOrientation(boolean canRecentsActivityRotate,
+        LinearLayout taskMenuLayout) {
         return LinearLayout.HORIZONTAL;
     }
 
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index b650526..f88cdb3 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -94,7 +94,7 @@
     float getTaskMenuX(float x, View thumbnailView);
     float getTaskMenuY(float y, View thumbnailView);
     int getTaskMenuWidth(View view);
-    int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout);
+    int getTaskMenuLayoutOrientation(boolean canRecentsActivityRotate, LinearLayout taskMenuLayout);
     void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp);
     int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect);
 
@@ -109,6 +109,7 @@
         public int halfPageSize;
         public int screenCenter;
         public int halfScreenSize;
+        public float pageParentScale;
     }
 
     class ChildBounds {
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index e87c887..f18b109 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -70,6 +70,7 @@
         out.halfPageSize = view.getNormalChildWidth() / 2;
         out.halfScreenSize = view.getMeasuredWidth() / 2;
         out.screenCenter = insets.left + view.getPaddingLeft() + out.scroll + out.halfPageSize;
+        out.pageParentScale = view.getScaleX();
     }
 
     @Override
@@ -237,8 +238,9 @@
     }
 
     @Override
-    public int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout) {
-        return taskMenuLayout.getOrientation();
+    public int getTaskMenuLayoutOrientation(boolean canRecentsActivityRotate,
+        LinearLayout taskMenuLayout) {
+        return canRecentsActivityRotate ? taskMenuLayout.getOrientation() : LinearLayout.VERTICAL;
     }
 
     @Override
diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java
index fc9f8f7..f6003dd 100644
--- a/src/com/android/launcher3/util/MainThreadInitializedObject.java
+++ b/src/com/android/launcher3/util/MainThreadInitializedObject.java
@@ -46,7 +46,7 @@
 
         if (mValue == null) {
             if (Looper.myLooper() == Looper.getMainLooper()) {
-                mValue = TraceHelper.whitelistIpcs("main.thread.object",
+                mValue = TraceHelper.allowIpcs("main.thread.object",
                         () -> mProvider.get(context.getApplicationContext()));
             } else {
                 try {
diff --git a/src/com/android/launcher3/util/OnboardingPrefs.java b/src/com/android/launcher3/util/OnboardingPrefs.java
index 26313e5..6e5e7d9 100644
--- a/src/com/android/launcher3/util/OnboardingPrefs.java
+++ b/src/com/android/launcher3/util/OnboardingPrefs.java
@@ -36,7 +36,6 @@
     public static final String SHELF_BOUNCE_SEEN = "launcher.shelf_bounce_seen";
     public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_count";
     public static final String SHELF_BOUNCE_COUNT = "launcher.shelf_bounce_count";
-    public static final String ALL_APPS_COUNT = "launcher.all_apps_count";
     public static final String HOTSEAT_DISCOVERY_TIP_COUNT = "launcher.hotseat_discovery_tip_count";
     public static final String HOTSEAT_LONGPRESS_TIP_SEEN = "launcher.hotseat_longpress_tip_seen";
 
@@ -56,7 +55,6 @@
     @StringDef(value = {
             HOME_BOUNCE_COUNT,
             SHELF_BOUNCE_COUNT,
-            ALL_APPS_COUNT,
             HOTSEAT_DISCOVERY_TIP_COUNT
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -67,7 +65,6 @@
         Map<String, Integer> maxCounts = new ArrayMap<>(4);
         maxCounts.put(HOME_BOUNCE_COUNT, 3);
         maxCounts.put(SHELF_BOUNCE_COUNT, 3);
-        maxCounts.put(ALL_APPS_COUNT, 5);
         maxCounts.put(HOTSEAT_DISCOVERY_TIP_COUNT, 5);
         MAX_COUNTS = Collections.unmodifiableMap(maxCounts);
     }
diff --git a/src/com/android/launcher3/util/SystemUiController.java b/src/com/android/launcher3/util/SystemUiController.java
index 275c024..3e48006 100644
--- a/src/com/android/launcher3/util/SystemUiController.java
+++ b/src/com/android/launcher3/util/SystemUiController.java
@@ -33,6 +33,7 @@
     public static final int UI_STATE_SCRIM_VIEW = 1;
     public static final int UI_STATE_WIDGET_BOTTOM_SHEET = 2;
     public static final int UI_STATE_OVERVIEW = 3;
+    public static final int UI_STATE_ALLAPPS = 4;
 
     public static final int FLAG_LIGHT_NAV = 1 << 0;
     public static final int FLAG_DARK_NAV = 1 << 1;
@@ -40,7 +41,7 @@
     public static final int FLAG_DARK_STATUS = 1 << 3;
 
     private final Window mWindow;
-    private final int[] mStates = new int[4];
+    private final int[] mStates = new int[5];
 
     public SystemUiController(Window window) {
         mWindow = window;
diff --git a/src/com/android/launcher3/util/TraceHelper.java b/src/com/android/launcher3/util/TraceHelper.java
index 168227d..c23df77 100644
--- a/src/com/android/launcher3/util/TraceHelper.java
+++ b/src/com/android/launcher3/util/TraceHelper.java
@@ -78,7 +78,7 @@
      * Temporarily ignore blocking binder calls for the duration of this {@link Supplier}.
      */
     @MainThread
-    public static <T> T whitelistIpcs(String rpcName, Supplier<T> supplier) {
+    public static <T> T allowIpcs(String rpcName, Supplier<T> supplier) {
         Object traceToken = INSTANCE.beginSection(rpcName, FLAG_IGNORE_BINDERS);
         try {
             return supplier.get();
diff --git a/src/com/android/launcher3/views/AccessibilityActionsView.java b/src/com/android/launcher3/views/AccessibilityActionsView.java
new file mode 100644
index 0000000..0eacaa3
--- /dev/null
+++ b/src/com/android/launcher3/views/AccessibilityActionsView.java
@@ -0,0 +1,97 @@
+/*
+ * 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.views;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.views.OptionsPopupView.OptionItem;
+
+/**
+ * Placeholder view to expose additional Launcher actions via accessibility actions
+ */
+public class AccessibilityActionsView extends View implements StateListener<LauncherState> {
+
+    public AccessibilityActionsView(Context context) {
+        this(context, null);
+    }
+
+    public AccessibilityActionsView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AccessibilityActionsView(Context context, @Nullable AttributeSet attrs,
+            int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        Launcher.getLauncher(context).getStateManager().addStateListener(this);
+        setWillNotDraw(true);
+    }
+
+    @Override
+    public void onStateTransitionComplete(LauncherState finalState) {
+        setImportantForAccessibility(finalState == NORMAL
+                ? IMPORTANT_FOR_ACCESSIBILITY_YES : IMPORTANT_FOR_ACCESSIBILITY_NO);
+    }
+
+    @Override
+    public AccessibilityNodeInfo createAccessibilityNodeInfo() {
+        AccessibilityNodeInfo info = super.createAccessibilityNodeInfo();
+        Launcher l = Launcher.getLauncher(getContext());
+        info.addAction(new AccessibilityAction(
+                R.string.all_apps_button_label, l.getText(R.string.all_apps_button_label)));
+        for (OptionItem item : OptionsPopupView.getOptions(l)) {
+            info.addAction(new AccessibilityAction(item.labelRes, l.getText(item.labelRes)));
+        }
+        return info;
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int action, Bundle arguments) {
+        if (super.performAccessibilityAction(action, arguments)) {
+            return true;
+        }
+        Launcher l = Launcher.getLauncher(getContext());
+        if (action == R.string.all_apps_button_label) {
+            l.getStateManager().goToState(ALL_APPS);
+            return true;
+        }
+        for (OptionItem item : OptionsPopupView.getOptions(l)) {
+            if (item.labelRes == action) {
+                if (item.eventId.getId() > 0) {
+                    l.getStatsLogManager().logger().log(item.eventId);
+                }
+                if (item.clickListener.onLongClick(this)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index 2c75c74..357eeb8 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -292,9 +292,6 @@
 
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "BaseDragLayer: " + ev);
-        }
         switch (ev.getAction()) {
             case ACTION_DOWN: {
                 if ((mTouchDispatchState & TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS) != 0) {
@@ -605,13 +602,13 @@
      */
     private boolean computeAllowSysuiScrims(@Nullable WallpaperInfo newWallpaperInfo) {
         if (newWallpaperInfo == null) {
-            // New wallpaper is static, not live. Thus, blacklist isn't applicable.
+            // Static wallpapers need scrim unless determined otherwise by wallpaperColors.
             return true;
         }
         ComponentName newWallpaper = newWallpaperInfo.getComponent();
         for (String wallpaperWithoutScrim : mWallpapersWithoutSysuiScrims) {
             if (newWallpaper.equals(ComponentName.unflattenFromString(wallpaperWithoutScrim))) {
-                // New wallpaper is blacklisted from showing a scrim.
+                // New wallpaper does not need a scrim.
                 return false;
             }
         }
diff --git a/src/com/android/launcher3/views/ClipIconView.java b/src/com/android/launcher3/views/ClipIconView.java
index 1a8e11b..fab0bd4 100644
--- a/src/com/android/launcher3/views/ClipIconView.java
+++ b/src/com/android/launcher3/views/ClipIconView.java
@@ -36,6 +36,7 @@
 import android.os.Build;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.ViewGroup.MarginLayoutParams;
 import android.view.ViewOutlineProvider;
 
 import androidx.annotation.Nullable;
@@ -44,8 +45,6 @@
 import androidx.dynamicanimation.animation.SpringForce;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.InsettableFrameLayout.LayoutParams;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
@@ -94,7 +93,6 @@
                 }
             };
 
-    private final Launcher mLauncher;
     private final int mBlurSizeOutline;
     private final boolean mIsRtl;
 
@@ -128,7 +126,6 @@
 
     public ClipIconView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mLauncher = Launcher.getLauncher(context);
         mBlurSizeOutline = getResources().getDimensionPixelSize(
                 R.dimen.blur_size_medium_outline);
         mIsRtl = Utilities.isRtl(getResources());
@@ -143,10 +140,41 @@
                         .setStiffness(SpringForce.STIFFNESS_LOW));
     }
 
-    void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
-            boolean isOpening, float scale, float minSize, LayoutParams parentLp,
-            boolean isVerticalBarLayout) {
-        DeviceProfile dp = mLauncher.getDeviceProfile();
+    /**
+     * Update the icon UI to match the provided parameters during an animation frame
+     */
+    public void update(RectF rect, float progress, float shapeProgressStart,
+            float cornerRadius, boolean isOpening, View container,
+            DeviceProfile dp, boolean isVerticalBarLayout) {
+
+        MarginLayoutParams lp = (MarginLayoutParams) container.getLayoutParams();
+
+        float dX = mIsRtl
+                ? rect.left - (dp.widthPx - lp.getMarginStart() - lp.width)
+                : rect.left - lp.getMarginStart();
+        float dY = rect.top - lp.topMargin;
+        container.setTranslationX(dX);
+        container.setTranslationY(dY);
+
+        float minSize = Math.min(lp.width, lp.height);
+        float scaleX = rect.width() / minSize;
+        float scaleY = rect.height() / minSize;
+        float scale = Math.max(1f, Math.min(scaleX, scaleY));
+
+        update(rect, progress, shapeProgressStart, cornerRadius, isOpening, scale,
+                minSize, lp, isVerticalBarLayout, dp);
+
+        container.setPivotX(0);
+        container.setPivotY(0);
+        container.setScaleX(scale);
+        container.setScaleY(scale);
+
+        container.invalidate();
+    }
+
+    private void update(RectF rect, float progress, float shapeProgressStart, float cornerRadius,
+            boolean isOpening, float scale, float minSize, MarginLayoutParams parentLp,
+            boolean isVerticalBarLayout, DeviceProfile dp) {
         float dX = mIsRtl
                 ? rect.left - (dp.widthPx - parentLp.getMarginStart() - parentLp.width)
                 : rect.left - parentLp.getMarginStart();
@@ -228,8 +256,11 @@
         }
     }
 
-    void setIcon(@Nullable Drawable drawable, int iconOffset, LayoutParams lp, boolean isOpening,
-            boolean isVerticalBarLayout) {
+    /**
+     * Sets the icon for this view as part of initial setup
+     */
+    public void setIcon(@Nullable Drawable drawable, int iconOffset, MarginLayoutParams lp,
+            boolean isOpening, boolean isVerticalBarLayout, DeviceProfile dp) {
         mIsAdaptiveIcon = drawable instanceof AdaptiveIconDrawable;
         if (mIsAdaptiveIcon) {
             boolean isFolderIcon = drawable instanceof FolderAdaptiveIcon;
@@ -264,15 +295,14 @@
                 Utilities.scaleRectAboutCenter(mStartRevealRect, IconShape.getNormalizationScale());
             }
 
-            float aspectRatio = mLauncher.getDeviceProfile().aspectRatio;
             if (isVerticalBarLayout) {
-                lp.width = (int) Math.max(lp.width, lp.height * aspectRatio);
+                lp.width = (int) Math.max(lp.width, lp.height * dp.aspectRatio);
             } else {
-                lp.height = (int) Math.max(lp.height, lp.width * aspectRatio);
+                lp.height = (int) Math.max(lp.height, lp.width * dp.aspectRatio);
             }
 
             int left = mIsRtl
-                    ? mLauncher.getDeviceProfile().widthPx - lp.getMarginStart() - lp.width
+                    ? dp.widthPx - lp.getMarginStart() - lp.width
                     : lp.leftMargin;
             layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
 
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 177aff4..8186dfa 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.Utilities.getFullDrawable;
 import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -47,7 +48,6 @@
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
@@ -144,32 +144,8 @@
     public void update(RectF rect, float alpha, float progress, float shapeProgressStart,
             float cornerRadius, boolean isOpening) {
         setAlpha(alpha);
-
-        InsettableFrameLayout.LayoutParams lp =
-                (InsettableFrameLayout.LayoutParams) getLayoutParams();
-
-        DeviceProfile dp = mLauncher.getDeviceProfile();
-        float dX = mIsRtl
-                ? rect.left - (dp.widthPx - lp.getMarginStart() - lp.width)
-                : rect.left - lp.getMarginStart();
-        float dY = rect.top - lp.topMargin;
-        setTranslationX(dX);
-        setTranslationY(dY);
-
-        float minSize = Math.min(lp.width, lp.height);
-        float scaleX = rect.width() / minSize;
-        float scaleY = rect.height() / minSize;
-        float scale = Math.max(1f, Math.min(scaleX, scaleY));
-
-        mClipIconView.update(rect, progress, shapeProgressStart, cornerRadius, isOpening, scale,
-                minSize, lp, mIsVerticalBarLayout);
-
-        setPivotX(0);
-        setPivotY(0);
-        setScaleX(scale);
-        setScaleY(scale);
-
-        invalidate();
+        mClipIconView.update(rect, progress, shapeProgressStart, cornerRadius, isOpening,
+                this, mLauncher.getDeviceProfile(), mIsVerticalBarLayout);
     }
 
     @Override
@@ -220,13 +196,18 @@
         layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
     }
 
+    private static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
+            RectF outRect) {
+        getLocationBoundsForView(launcher, v, isOpening, outRect, new Rect());
+    }
+
     /**
      * Gets the location bounds of a view and returns the overall rotation.
      * - For DeepShortcutView, we return the bounds of the icon view.
      * - For BubbleTextView, we return the icon bounds.
      */
-    private static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
-            RectF outRect) {
+    public static void getLocationBoundsForView(Launcher launcher, View v, boolean isOpening,
+            RectF outRect, Rect outViewBounds) {
         boolean ignoreTransform = !isOpening;
         if (v instanceof DeepShortcutView) {
             v = ((DeepShortcutView) v).getBubbleText();
@@ -239,17 +220,16 @@
             return;
         }
 
-        Rect iconBounds = new Rect();
         if (v instanceof BubbleTextView) {
-            ((BubbleTextView) v).getIconBounds(iconBounds);
+            ((BubbleTextView) v).getIconBounds(outViewBounds);
         } else if (v instanceof FolderIcon) {
-            ((FolderIcon) v).getPreviewBounds(iconBounds);
+            ((FolderIcon) v).getPreviewBounds(outViewBounds);
         } else {
-            iconBounds.set(0, 0, v.getWidth(), v.getHeight());
+            outViewBounds.set(0, 0, v.getWidth(), v.getHeight());
         }
 
-        float[] points = new float[] {iconBounds.left, iconBounds.top, iconBounds.right,
-                iconBounds.bottom};
+        float[] points = new float[] {outViewBounds.left, outViewBounds.top, outViewBounds.right,
+                outViewBounds.bottom};
         Utilities.getDescendantCoordRelativeToAncestor(v, launcher.getDragLayer(), points,
                 false, ignoreTransform);
         outRect.set(
@@ -336,7 +316,8 @@
         final InsettableFrameLayout.LayoutParams lp =
                 (InsettableFrameLayout.LayoutParams) getLayoutParams();
         mBadge = badge;
-        mClipIconView.setIcon(drawable, iconOffset, lp, mIsOpening, mIsVerticalBarLayout);
+        mClipIconView.setIcon(drawable, iconOffset, lp, mIsOpening, mIsVerticalBarLayout,
+                mLauncher.getDeviceProfile());
         if (drawable instanceof AdaptiveIconDrawable) {
             final int originalHeight = lp.height;
             final int originalWidth = lp.width;
@@ -381,7 +362,7 @@
             if (mIconLoadResult.isIconLoaded) {
                 setIcon(mIconLoadResult.drawable, mIconLoadResult.badge,
                         mIconLoadResult.iconOffset);
-                hideOriginalView(originalView);
+                setIconAndDotVisible(originalView, false);
             } else {
                 mIconLoadResult.onIconLoaded = () -> {
                     if (cancellationSignal.isCanceled()) {
@@ -392,22 +373,13 @@
                             mIconLoadResult.iconOffset);
 
                     setVisibility(VISIBLE);
-                    hideOriginalView(originalView);
+                    setIconAndDotVisible(originalView, false);
                 };
                 mLoadIconSignal = cancellationSignal;
             }
         }
     }
 
-    private void hideOriginalView(View originalView) {
-        if (originalView instanceof IconLabelDotView) {
-            ((IconLabelDotView) originalView).setIconVisible(false);
-            ((IconLabelDotView) originalView).setForceHideDot(true);
-        } else {
-            originalView.setVisibility(INVISIBLE);
-        }
-    }
-
     @WorkerThread
     @SuppressWarnings("WrongThread")
     private static int getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position) {
@@ -477,7 +449,7 @@
         }
         if (!mIsOpening) {
             // When closing an app, we want the item on the workspace to be invisible immediately
-            hideOriginalView(mOriginalIcon);
+            setIconAndDotVisible(mOriginalIcon, false);
         }
     }
 
@@ -573,12 +545,7 @@
 
             if (hideOriginal) {
                 if (isOpening) {
-                    if (originalView instanceof BubbleTextView) {
-                        ((BubbleTextView) originalView).setIconVisible(true);
-                        ((BubbleTextView) originalView).setForceHideDot(false);
-                    } else {
-                        originalView.setVisibility(VISIBLE);
-                    }
+                    setIconAndDotVisible(originalView, true);
                     view.finish(dragLayer);
                 } else {
                     view.mFadeAnimatorSet = view.createFadeAnimation(originalView, dragLayer);
@@ -615,12 +582,10 @@
         });
 
         if (originalView instanceof IconLabelDotView) {
-            IconLabelDotView view = (IconLabelDotView) originalView;
             fade.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
-                    view.setIconVisible(true);
-                    view.setForceHideDot(false);
+                    setIconAndDotVisible(originalView, true);
                 }
             });
         }
diff --git a/src/com/android/launcher3/views/FloatingSurfaceView.java b/src/com/android/launcher3/views/FloatingSurfaceView.java
new file mode 100644
index 0000000..040619e
--- /dev/null
+++ b/src/com/android/launcher3/views/FloatingSurfaceView.java
@@ -0,0 +1,241 @@
+/*
+ * 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.views;
+
+import static com.android.launcher3.views.FloatingIconView.getLocationBoundsForView;
+import static com.android.launcher3.views.IconLabelDotView.setIconAndDotVisible;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Picture;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.GestureNavContract;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.util.DefaultDisplay;
+import com.android.launcher3.util.Executors;
+
+/**
+ * Similar to {@link FloatingIconView} but displays a surface with the targetIcon. It then passes
+ * the surfaceHandle to the {@link GestureNavContract}.
+ */
+@TargetApi(Build.VERSION_CODES.R)
+public class FloatingSurfaceView extends AbstractFloatingView implements
+        OnGlobalLayoutListener, Insettable, SurfaceHolder.Callback2 {
+
+    private final RectF mTmpPosition = new RectF();
+
+    private final Launcher mLauncher;
+    private final RectF mIconPosition = new RectF();
+
+    private final Rect mIconBounds = new Rect();
+    private final Picture mPicture = new Picture();
+    private final Runnable mRemoveViewRunnable = this::removeViewFromParent;
+
+    private final SurfaceView mSurfaceView;
+
+
+    private View mIcon;
+    private GestureNavContract mContract;
+
+    public FloatingSurfaceView(Context context) {
+        this(context, null);
+    }
+
+    public FloatingSurfaceView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public FloatingSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mLauncher = Launcher.getLauncher(context);
+
+        mSurfaceView = new SurfaceView(context);
+        mSurfaceView.setZOrderOnTop(true);
+
+        mSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
+        mSurfaceView.getHolder().addCallback(this);
+        mIsOpen = true;
+        addView(mSurfaceView);
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        setCurrentIconVisible(true);
+        mLauncher.getViewCache().recycleView(R.layout.floating_surface_view, this);
+        mContract = null;
+        mIcon = null;
+        mIsOpen = false;
+
+        // Remove after some time, to avoid flickering
+        Executors.MAIN_EXECUTOR.getHandler().postDelayed(mRemoveViewRunnable,
+                DefaultDisplay.INSTANCE.get(mLauncher).getInfo().singleFrameMs);
+    }
+
+    private void removeViewFromParent() {
+        mPicture.beginRecording(1, 1);
+        mPicture.endRecording();
+        mLauncher.getDragLayer().removeView(this);
+    }
+
+    /**
+     * Shows the surfaceView for the provided contract
+     */
+    public static void show(Launcher launcher, GestureNavContract contract) {
+        FloatingSurfaceView view = launcher.getViewCache().getView(R.layout.floating_surface_view,
+                launcher, launcher.getDragLayer());
+        view.mContract = contract;
+        view.mIsOpen = true;
+
+        // Cancel any pending remove
+        Executors.MAIN_EXECUTOR.getHandler().removeCallbacks(view.mRemoveViewRunnable);
+        view.removeViewFromParent();
+        launcher.getDragLayer().addView(view);
+    }
+
+    @Override
+    public void logActionCommand(int command) { }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_ICON_SURFACE) != 0;
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        close(false);
+        return false;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        getViewTreeObserver().addOnGlobalLayoutListener(this);
+        updateIconLocation();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        getViewTreeObserver().removeOnGlobalLayoutListener(this);
+        setCurrentIconVisible(true);
+    }
+
+    @Override
+    public void onGlobalLayout() {
+        updateIconLocation();
+    }
+
+    @Override
+    public void setInsets(Rect insets) { }
+
+    private void updateIconLocation() {
+        if (mContract == null) {
+            return;
+        }
+        View icon = mLauncher.getWorkspace().getFirstMatchForAppClose(
+                mContract.componentName.getPackageName(), mContract.user);
+
+        boolean iconChanged = mIcon != icon;
+        if (iconChanged) {
+            setCurrentIconVisible(true);
+            mIcon = icon;
+            setCurrentIconVisible(false);
+        }
+
+        if (icon != null && icon.isAttachedToWindow()) {
+            getLocationBoundsForView(mLauncher, icon, false, mTmpPosition, mIconBounds);
+
+            if (!mTmpPosition.equals(mIconPosition)) {
+                mIconPosition.set(mTmpPosition);
+                sendIconInfo();
+
+                LayoutParams lp = (LayoutParams) mSurfaceView.getLayoutParams();
+                lp.width = Math.round(mIconPosition.width());
+                lp.height = Math.round(mIconPosition.height());
+                lp.leftMargin = Math.round(mIconPosition.left);
+                lp.topMargin = Math.round(mIconPosition.top);
+            }
+        }
+        if (iconChanged && !mIconBounds.isEmpty()) {
+            // Record the icon display
+            setCurrentIconVisible(true);
+            Canvas c = mPicture.beginRecording(mIconBounds.width(), mIconBounds.height());
+            c.translate(-mIconBounds.left, -mIconBounds.top);
+            mIcon.draw(c);
+            mPicture.endRecording();
+            setCurrentIconVisible(false);
+            drawOnSurface();
+        }
+    }
+
+    private void sendIconInfo() {
+        if (mContract != null && !mIconPosition.isEmpty()) {
+            mContract.sendEndPosition(mIconPosition, mSurfaceView.getSurfaceControl());
+        }
+    }
+
+    @Override
+    public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
+        drawOnSurface();
+        sendIconInfo();
+    }
+
+    @Override
+    public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder,
+            int format, int width, int height) {
+        drawOnSurface();
+    }
+
+    @Override
+    public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {}
+
+    @Override
+    public void surfaceRedrawNeeded(@NonNull SurfaceHolder surfaceHolder) {
+        drawOnSurface();
+    }
+
+    private void drawOnSurface() {
+        SurfaceHolder surfaceHolder = mSurfaceView.getHolder();
+
+        Canvas c = surfaceHolder.lockHardwareCanvas();
+        if (c != null) {
+            mPicture.draw(c);
+            surfaceHolder.unlockCanvasAndPost(c);
+        }
+    }
+
+    private void setCurrentIconVisible(boolean isVisible) {
+        if (mIcon != null) {
+            setIconAndDotVisible(mIcon, isVisible);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/views/IconLabelDotView.java b/src/com/android/launcher3/views/IconLabelDotView.java
index 057caaf..e9113cf 100644
--- a/src/com/android/launcher3/views/IconLabelDotView.java
+++ b/src/com/android/launcher3/views/IconLabelDotView.java
@@ -15,10 +15,24 @@
  */
 package com.android.launcher3.views;
 
+import android.view.View;
+
 /**
  * A view that has an icon, label, and notification dot.
  */
 public interface IconLabelDotView {
     void setIconVisible(boolean visible);
     void setForceHideDot(boolean hide);
+
+    /**
+     * Sets the visibility of icon and dot of the view
+     */
+    static void setIconAndDotVisible(View view, boolean visible) {
+        if (view instanceof IconLabelDotView) {
+            ((IconLabelDotView) view).setIconVisible(visible);
+            ((IconLabelDotView) view).setForceHideDot(!visible);
+        } else {
+            view.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 049a1ac..e95dc5b 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -85,10 +85,10 @@
         if (item == null) {
             return false;
         }
-        if (item.mEventId.getId() > 0) {
-            mLauncher.getStatsLogManager().logger().log(item.mEventId);
+        if (item.eventId.getId() > 0) {
+            mLauncher.getStatsLogManager().logger().log(item.eventId);
         }
-        if (item.mClickListener.onLongClick(view)) {
+        if (item.clickListener.onLongClick(view)) {
             close(true);
             return true;
         }
@@ -130,8 +130,8 @@
         for (OptionItem item : items) {
             DeepShortcutView view =
                     (DeepShortcutView) popup.inflateAndAdd(R.layout.system_shortcut, popup);
-            view.getIconView().setBackgroundResource(item.mIconRes);
-            view.getBubbleText().setText(item.mLabelRes);
+            view.getIconView().setBackgroundResource(item.iconRes);
+            view.getBubbleText().setText(item.labelRes);
             view.setDividerVisibility(View.INVISIBLE);
             view.setOnClickListener(popup);
             view.setOnLongClickListener(popup);
@@ -152,7 +152,13 @@
             y = launcher.getDragLayer().getHeight() / 2;
         }
         RectF target = new RectF(x - halfSize, y - halfSize, x + halfSize, y + halfSize);
+        show(launcher, target, getOptions(launcher));
+    }
 
+    /**
+     * Returns the list of supported actions
+     */
+    public static ArrayList<OptionItem> getOptions(Launcher launcher) {
         ArrayList<OptionItem> options = new ArrayList<>();
         int resString = Utilities.existsStyleWallpapers(launcher) ?
                 R.string.styles_wallpaper_button_text : R.string.wallpaper_button_text;
@@ -170,10 +176,10 @@
                 LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS,
                 OptionsPopupView::startSettings));
 
-        show(launcher, target, options);
+        return options;
     }
 
-    public static boolean onWidgetsClicked(View view) {
+    private static boolean onWidgetsClicked(View view) {
         return openWidgets(Launcher.getLauncher(view.getContext())) != null;
     }
 
@@ -188,7 +194,7 @@
         }
     }
 
-    public static boolean startSettings(View view) {
+    private static boolean startSettings(View view) {
         TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: startSettings");
         Launcher launcher = Launcher.getLauncher(view.getContext());
         launcher.startActivity(new Intent(Intent.ACTION_APPLICATION_PREFERENCES)
@@ -201,7 +207,7 @@
      * Event handler for the wallpaper picker button that appears after a long press
      * on the home screen.
      */
-    public static boolean startWallpaperPicker(View v) {
+    private static boolean startWallpaperPicker(View v) {
         Launcher launcher = Launcher.getLauncher(v.getContext());
         if (!Utilities.isWallpaperAllowed(launcher)) {
             Toast.makeText(launcher, R.string.msg_disabled_by_admin, Toast.LENGTH_SHORT).show();
@@ -233,17 +239,17 @@
 
     public static class OptionItem {
 
-        private final int mLabelRes;
-        private final int mIconRes;
-        private final EventEnum mEventId;
-        private final OnLongClickListener mClickListener;
+        public final int labelRes;
+        public final int iconRes;
+        public final EventEnum eventId;
+        public final OnLongClickListener clickListener;
 
         public OptionItem(int labelRes, int iconRes, EventEnum eventId,
                 OnLongClickListener clickListener) {
-            mLabelRes = labelRes;
-            mIconRes = iconRes;
-            mEventId = eventId;
-            mClickListener = clickListener;
+            this.labelRes = labelRes;
+            this.iconRes = iconRes;
+            this.eventId = eventId;
+            this.clickListener = clickListener;
         }
     }
 }
diff --git a/src/com/android/launcher3/views/ScrimView.java b/src/com/android/launcher3/views/ScrimView.java
index 22faf97..7f0765b 100644
--- a/src/com/android/launcher3/views/ScrimView.java
+++ b/src/com/android/launcher3/views/ScrimView.java
@@ -15,119 +15,37 @@
  */
 package com.android.launcher3.views;
 
-import static android.content.Context.ACCESSIBILITY_SERVICE;
-import static android.view.MotionEvent.ACTION_DOWN;
-
 import static androidx.core.graphics.ColorUtils.compositeColors;
 
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.anim.Interpolators.clampToProgress;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_SCRIM_VIEW;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.Keyframe;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.animation.RectEvaluator;
 import android.content.Context;
-import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Color;
-import android.graphics.Point;
 import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
 import android.util.AttributeSet;
-import android.util.IntProperty;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
 import android.view.View;
-import android.view.accessibility.AccessibilityManager;
-import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.core.graphics.ColorUtils;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
-import androidx.customview.widget.ExploreByTouchHelper;
 
-import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.uioverrides.WallpaperColorInfo;
 import com.android.launcher3.uioverrides.WallpaperColorInfo.OnChangeListener;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
 import com.android.launcher3.util.Themes;
-import com.android.launcher3.widget.WidgetsFullSheet;
-
-import java.util.List;
 
 /**
  * Simple scrim which draws a flat color
  */
-public class ScrimView<T extends Launcher> extends View implements Insettable, OnChangeListener,
-        AccessibilityStateChangeListener {
-
-    public static final IntProperty<ScrimView> DRAG_HANDLE_ALPHA =
-            new IntProperty<ScrimView>("dragHandleAlpha") {
-
-                @Override
-                public Integer get(ScrimView scrimView) {
-                    return scrimView.mDragHandleAlpha;
-                }
-
-                @Override
-                public void setValue(ScrimView scrimView, int value) {
-                    scrimView.setDragHandleAlpha(value);
-                }
-            };
-    private static final int WALLPAPERS = R.string.wallpaper_button_text;
-    private static final int WIDGETS = R.string.widget_button_text;
-    private static final int SETTINGS = R.string.settings_button_text;
-    private static final int ALPHA_CHANNEL_COUNT = 1;
-
-    private static final long DRAG_HANDLE_BOUNCE_DURATION_MS = 300;
-    // How much to delay before repeating the bounce.
-    private static final long DRAG_HANDLE_BOUNCE_DELAY_MS = 200;
-    // Repeat this many times (i.e. total number of bounces is 1 + this).
-    private static final int DRAG_HANDLE_BOUNCE_REPEAT_COUNT = 2;
-
-    private final Rect mTempRect = new Rect();
-    private final int[] mTempPos = new int[2];
+public class ScrimView<T extends Launcher> extends View implements Insettable, OnChangeListener {
 
     protected final T mLauncher;
     private final WallpaperColorInfo mWallpaperColorInfo;
-    private final AccessibilityManager mAM;
     protected final int mEndScrim;
     protected final boolean mIsScrimDark;
 
-    private final StateListener<LauncherState> mAccessibilityLauncherStateListener =
-            new StateListener<LauncherState>() {
-        @Override
-        public void onStateTransitionComplete(LauncherState finalState) {
-            setImportantForAccessibility(finalState == ALL_APPS
-                    ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                    : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-        }
-    };
-
     protected float mMaxScrimAlpha;
 
     protected float mProgress = 1;
@@ -137,22 +55,6 @@
     protected int mEndFlatColor;
     protected int mEndFlatColorAlpha;
 
-    protected final Point mDragHandleSize;
-    private final int mDragHandleTouchSize;
-    private final int mDragHandlePaddingInVerticalBarLayout;
-    protected float mDragHandleOffset;
-    private final Rect mDragHandleBounds;
-    private final RectF mHitRect = new RectF();
-    private ObjectAnimator mDragHandleAnim;
-
-    private final MultiValueAlpha mMultiValueAlpha;
-
-    private final AccessibilityHelper mAccessibilityHelper;
-    @Nullable
-    protected Drawable mDragHandle;
-
-    private int mDragHandleAlpha = 255;
-
     public ScrimView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mLauncher = Launcher.cast(Launcher.getLauncher(context));
@@ -161,59 +63,24 @@
         mIsScrimDark = ColorUtils.calculateLuminance(mEndScrim) < 0.5f;
 
         mMaxScrimAlpha = 0.7f;
-
-        Resources res = context.getResources();
-        mDragHandleSize = new Point(res.getDimensionPixelSize(R.dimen.vertical_drag_handle_width),
-                res.getDimensionPixelSize(R.dimen.vertical_drag_handle_height));
-        mDragHandleBounds = new Rect(0, 0, mDragHandleSize.x, mDragHandleSize.y);
-        mDragHandleTouchSize = res.getDimensionPixelSize(R.dimen.vertical_drag_handle_touch_size);
-        mDragHandlePaddingInVerticalBarLayout = context.getResources()
-                .getDimensionPixelSize(R.dimen.vertical_drag_handle_padding_in_vertical_bar_layout);
-
-        mAccessibilityHelper = createAccessibilityHelper();
-        ViewCompat.setAccessibilityDelegate(this, mAccessibilityHelper);
-
-        mAM = (AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE);
         setFocusable(false);
-        mMultiValueAlpha = new MultiValueAlpha(this, ALPHA_CHANNEL_COUNT);
-    }
-
-    public AlphaProperty getAlphaProperty(int index) {
-        return mMultiValueAlpha.getProperty(index);
-    }
-
-    @NonNull
-    protected AccessibilityHelper createAccessibilityHelper() {
-        return new AccessibilityHelper();
     }
 
     @Override
-    public void setInsets(Rect insets) {
-        updateDragHandleBounds();
-        updateDragHandleVisibility();
-    }
+    public void setInsets(Rect insets) { }
 
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-        updateDragHandleBounds();
-    }
 
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         mWallpaperColorInfo.addOnChangeListener(this);
         onExtractedColorsChanged(mWallpaperColorInfo);
-
-        mAM.addAccessibilityStateChangeListener(this);
-        onAccessibilityStateChanged(mAM.isEnabled());
     }
 
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         mWallpaperColorInfo.removeOnChangeListener(this);
-        mAM.removeAccessibilityStateChangeListener(this);
     }
 
     @Override
@@ -234,10 +101,8 @@
     public void setProgress(float progress) {
         if (mProgress != progress) {
             mProgress = progress;
-            stopDragHandleEducationAnim();
             updateColors();
             updateSysUiColors();
-            updateDragHandleAlpha();
             invalidate();
         }
     }
@@ -260,286 +125,10 @@
         }
     }
 
-    protected void updateDragHandleAlpha() {
-        if (mDragHandle != null) {
-            mDragHandle.setAlpha(mDragHandleAlpha);
-        }
-    }
-
-    private void setDragHandleAlpha(int alpha) {
-        if (alpha != mDragHandleAlpha) {
-            mDragHandleAlpha = alpha;
-            if (mDragHandle != null) {
-                mDragHandle.setAlpha(mDragHandleAlpha);
-                invalidate();
-            }
-        }
-    }
-
     @Override
     protected void onDraw(Canvas canvas) {
         if (mCurrentFlatColor != 0) {
             canvas.drawColor(mCurrentFlatColor);
         }
-        drawDragHandle(canvas);
-    }
-
-    protected void drawDragHandle(Canvas canvas) {
-        if (mDragHandle != null) {
-            canvas.translate(0, -mDragHandleOffset);
-            mDragHandle.draw(canvas);
-            canvas.translate(0, mDragHandleOffset);
-        }
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        boolean superHandledTouch = super.onTouchEvent(event);
-        if (event.getAction() == ACTION_DOWN) {
-            if (!superHandledTouch && mHitRect.contains(event.getX(), event.getY())) {
-                if (startDragHandleEducationAnim()) {
-                    return true;
-                }
-            }
-            stopDragHandleEducationAnim();
-        }
-        return superHandledTouch;
-    }
-
-    /**
-     * Animates the drag handle to demonstrate how to get to all apps.
-     * @return Whether the animation was started (false if drag handle is invisible).
-     */
-    public boolean startDragHandleEducationAnim() {
-        stopDragHandleEducationAnim();
-
-        if (mDragHandle == null || mDragHandle.getAlpha() != 255) {
-            return false;
-        }
-
-        final Drawable drawable = mDragHandle;
-        mDragHandle = null;
-
-        Rect bounds = new Rect(mDragHandleBounds);
-        bounds.offset(0, -(int) mDragHandleOffset);
-        drawable.setBounds(bounds);
-
-        Rect topBounds = new Rect(bounds);
-        topBounds.offset(0, -bounds.height());
-
-        Rect invalidateRegion = new Rect(bounds);
-        invalidateRegion.top = topBounds.top;
-
-        final float progressToReachTop = 0.6f;
-        Keyframe frameTop = Keyframe.ofObject(progressToReachTop, topBounds);
-        frameTop.setInterpolator(DEACCEL);
-        Keyframe frameBot = Keyframe.ofObject(1, bounds);
-        frameBot.setInterpolator(ACCEL_DEACCEL);
-        PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("bounds",
-                Keyframe.ofObject(0, bounds), frameTop, frameBot);
-        holder.setEvaluator(new RectEvaluator());
-
-        mDragHandleAnim = ObjectAnimator.ofPropertyValuesHolder(drawable, holder);
-        long totalBounceDuration = DRAG_HANDLE_BOUNCE_DURATION_MS + DRAG_HANDLE_BOUNCE_DELAY_MS;
-        // The bounce finishes by this progress, the rest of the duration just delays next bounce.
-        float delayStartProgress = 1f - (float) DRAG_HANDLE_BOUNCE_DELAY_MS / totalBounceDuration;
-        mDragHandleAnim.addUpdateListener((v) -> invalidate(invalidateRegion));
-        mDragHandleAnim.setDuration(totalBounceDuration);
-        mDragHandleAnim.setInterpolator(clampToProgress(LINEAR, 0, delayStartProgress));
-        mDragHandleAnim.setRepeatCount(DRAG_HANDLE_BOUNCE_REPEAT_COUNT);
-        getOverlay().add(drawable);
-
-        mDragHandleAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mDragHandleAnim = null;
-                getOverlay().remove(drawable);
-                updateDragHandleVisibility(drawable);
-            }
-        });
-        mDragHandleAnim.start();
-        return true;
-    }
-
-    private void stopDragHandleEducationAnim() {
-        if (mDragHandleAnim != null) {
-            mDragHandleAnim.end();
-        }
-    }
-
-    protected void updateDragHandleBounds() {
-        DeviceProfile grid = mLauncher.getDeviceProfile();
-        final int left;
-        final int width = getMeasuredWidth();
-        final int top = getMeasuredHeight() - mDragHandleSize.y - grid.getInsets().bottom;
-        final int topMargin;
-
-        if (grid.isVerticalBarLayout()) {
-            topMargin = grid.workspacePadding.bottom + mDragHandlePaddingInVerticalBarLayout;
-            if (grid.isSeascape()) {
-                left = width - grid.getInsets().right - mDragHandleSize.x
-                        - mDragHandlePaddingInVerticalBarLayout;
-            } else {
-                left = grid.getInsets().left + mDragHandlePaddingInVerticalBarLayout;
-            }
-        } else {
-            left = Math.round((width - mDragHandleSize.x) / 2f);
-            topMargin = grid.hotseatBarSizePx;
-        }
-        mDragHandleBounds.offsetTo(left, top - topMargin);
-        mHitRect.set(mDragHandleBounds);
-        // Inset outwards to increase touch size.
-        mHitRect.inset((mDragHandleSize.x - mDragHandleTouchSize) / 2f,
-                (mDragHandleSize.y - mDragHandleTouchSize) / 2f);
-
-        if (mDragHandle != null) {
-            mDragHandle.setBounds(mDragHandleBounds);
-        }
-    }
-
-    @Override
-    public void onAccessibilityStateChanged(boolean enabled) {
-        StateManager<LauncherState> stateManager = mLauncher.getStateManager();
-        stateManager.removeStateListener(mAccessibilityLauncherStateListener);
-
-        if (enabled) {
-            stateManager.addStateListener(mAccessibilityLauncherStateListener);
-            mAccessibilityLauncherStateListener.onStateTransitionComplete(stateManager.getState());
-        } else {
-            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
-        }
-        updateDragHandleVisibility();
-    }
-
-    public void updateDragHandleVisibility() {
-        updateDragHandleVisibility(null);
-    }
-
-    private void updateDragHandleVisibility(@Nullable Drawable recycle) {
-        boolean visible = shouldDragHandleBeVisible();
-        boolean wasVisible = mDragHandle != null;
-        if (visible != wasVisible) {
-            if (visible) {
-                mDragHandle = recycle != null ? recycle :
-                        mLauncher.getDrawable(R.drawable.drag_handle_indicator_shadow);
-                mDragHandle.setBounds(mDragHandleBounds);
-
-                updateDragHandleAlpha();
-            } else {
-                mDragHandle = null;
-            }
-            invalidate();
-        }
-    }
-
-    protected boolean shouldDragHandleBeVisible() {
-        return mLauncher.getDeviceProfile().isVerticalBarLayout() || mAM.isEnabled();
-    }
-
-    @Override
-    public boolean dispatchHoverEvent(MotionEvent event) {
-        return mAccessibilityHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
-    }
-
-    @Override
-    public boolean dispatchKeyEvent(KeyEvent event) {
-        return mAccessibilityHelper.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
-    }
-
-    @Override
-    public void onFocusChanged(boolean gainFocus, int direction,
-            Rect previouslyFocusedRect) {
-        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
-        mAccessibilityHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
-    }
-
-    protected class AccessibilityHelper extends ExploreByTouchHelper {
-
-        private static final int DRAG_HANDLE_ID = 1;
-
-        public AccessibilityHelper() {
-            super(ScrimView.this);
-        }
-
-        @Override
-        protected int getVirtualViewAt(float x, float y) {
-            return  mHitRect.contains((int) x, (int) y)
-                    ? DRAG_HANDLE_ID : INVALID_ID;
-        }
-
-        @Override
-        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
-            virtualViewIds.add(DRAG_HANDLE_ID);
-        }
-
-        @Override
-        protected void onPopulateNodeForVirtualView(int virtualViewId,
-                AccessibilityNodeInfoCompat node) {
-            node.setContentDescription(getContext().getString(R.string.all_apps_button_label));
-            node.setBoundsInParent(mDragHandleBounds);
-
-            getLocationOnScreen(mTempPos);
-            mTempRect.set(mDragHandleBounds);
-            mTempRect.offset(mTempPos[0], mTempPos[1]);
-            node.setBoundsInScreen(mTempRect);
-
-            node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
-            node.setClickable(true);
-            node.setFocusable(true);
-
-            if (mLauncher.isInState(NORMAL)) {
-                Context context = getContext();
-                if (Utilities.isWallpaperAllowed(context)) {
-                    node.addAction(
-                            new AccessibilityActionCompat(WALLPAPERS, context.getText(WALLPAPERS)));
-                }
-                node.addAction(new AccessibilityActionCompat(WIDGETS, context.getText(WIDGETS)));
-                node.addAction(new AccessibilityActionCompat(SETTINGS, context.getText(SETTINGS)));
-            }
-        }
-
-        @Override
-        protected boolean onPerformActionForVirtualView(
-                int virtualViewId, int action, Bundle arguments) {
-            if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
-                mLauncher.getUserEventDispatcher().logActionOnControl(
-                        Action.Touch.TAP, ControlType.ALL_APPS_BUTTON,
-                        mLauncher.getStateManager().getState().containerType);
-                mLauncher.getStateManager().goToState(ALL_APPS);
-                return true;
-            } else if (action == WALLPAPERS) {
-                return OptionsPopupView.startWallpaperPicker(ScrimView.this);
-            } else if (action == WIDGETS) {
-                int originalImportanceForAccessibility = getImportantForAccessibility();
-                setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
-                WidgetsFullSheet widgetsFullSheet = OptionsPopupView.openWidgets(mLauncher);
-                if (widgetsFullSheet == null) {
-                    setImportantForAccessibility(originalImportanceForAccessibility);
-                    return false;
-                }
-                widgetsFullSheet.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
-                    @Override
-                    public void onViewAttachedToWindow(View view) {}
-
-                    @Override
-                    public void onViewDetachedFromWindow(View view) {
-                        setImportantForAccessibility(originalImportanceForAccessibility);
-                        widgetsFullSheet.removeOnAttachStateChangeListener(this);
-                    }
-                });
-                return true;
-            } else if (action == SETTINGS) {
-                return OptionsPopupView.startSettings(ScrimView.this);
-            }
-
-            return false;
-        }
-    }
-
-    /**
-     * @return The top of this scrim view, or {@link Float#MAX_VALUE} if there's no distinct top.
-     */
-    public float getVisualTop() {
-        return Float.MAX_VALUE;
     }
 }
diff --git a/src/com/android/launcher3/widget/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
index c9e80dc..df6e2c3 100644
--- a/src/com/android/launcher3/widget/WidgetsDiffReporter.java
+++ b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
@@ -20,10 +20,8 @@
 
 import androidx.recyclerview.widget.RecyclerView;
 
-import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.data.PackageItemInfo;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.widget.WidgetsListAdapter.WidgetListRowEntryComparator;
 
 import java.util.ArrayList;
@@ -34,8 +32,8 @@
  * methods accordingly.
  */
 public class WidgetsDiffReporter {
-    private static final boolean DEBUG = Utilities.IS_RUNNING_IN_TEST_HARNESS; // b/160238801
-    private static final String TAG = TestProtocol.NO_SCROLL_END_WIDGETS;
+    private static final boolean DEBUG = false;
+    private static final String TAG = "WidgetsDiffReporter";
 
     private final IconCache mIconCache;
     private final RecyclerView.Adapter mListener;
diff --git a/src/com/android/launcher3/widget/WidgetsFullSheet.java b/src/com/android/launcher3/widget/WidgetsFullSheet.java
index ba55f5a..68a3ec5 100644
--- a/src/com/android/launcher3/widget/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsFullSheet.java
@@ -24,7 +24,6 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -39,10 +38,8 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetHost.ProviderChangedListener;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 import com.android.launcher3.views.TopRoundedCornerView;
 
@@ -71,14 +68,6 @@
 
     }
 
-    @Override
-    public boolean dispatchTouchEvent(MotionEvent ev) {
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "WidgetsFullSheet: " + ev);
-        }
-        return super.dispatchTouchEvent(ev);
-    }
-
     public WidgetsFullSheet(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
index 8f81977..69de12b 100644
--- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
@@ -19,14 +19,11 @@
 import android.content.Context;
 import android.graphics.Point;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 
 import com.android.launcher3.BaseRecyclerView;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.testing.TestProtocol;
 
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
@@ -120,9 +117,6 @@
     public int getCurrentScrollY() {
         // Skip early if widgets are not bound.
         if (isModelNotReady() || getChildCount() == 0) {
-            if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-                Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "getCurrentScrollY: -1");
-            }
             return -1;
         }
 
@@ -131,10 +125,6 @@
         int y = (child.getMeasuredHeight() * rowIndex);
         int offset = getLayoutManager().getDecoratedTop(child);
 
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS,
-                    "getCurrentScrollY: " + (getPaddingTop() + y - offset));
-        }
         return getPaddingTop() + y - offset;
     }
 
@@ -166,22 +156,13 @@
         }
         if (mTouchDownOnScroller) {
             final boolean result = mScrollbar.handleTouchEvent(e, mFastScrollerOffset);
-            if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-                Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onInterceptTouchEvent 1 " + result);
-            }
             return result;
         }
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onInterceptTouchEvent 2 false");
-        }
         return false;
     }
 
     @Override
     public void onTouchEvent(RecyclerView rv, MotionEvent e) {
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "WidgetsRecyclerView.onTouchEvent");
-        }
         if (mTouchDownOnScroller) {
             mScrollbar.handleTouchEvent(e, mFastScrollerOffset);
         }
@@ -189,31 +170,5 @@
 
     @Override
     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onRequestDisallowInterceptTouchEvent "
-                    + disallowIntercept);
-        }
-    }
-
-    @Override
-    public boolean dispatchTouchEvent(MotionEvent ev) {
-        final boolean result = super.dispatchTouchEvent(ev);
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "WidgetsRecyclerView: state: "
-                    + getScrollState()
-                    + " can scroll: " + getLayoutManager().canScrollVertically()
-                    + " result: " + result
-                    + " layout suppressed: " + isLayoutSuppressed()
-                    + " event: " + ev);
-        }
-        return result;
-    }
-
-    @Override
-    public void stopNestedScroll() {
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "stopNestedScroll");
-        }
-        super.stopNestedScroll();
     }
 }
\ No newline at end of file
diff --git a/tests/Android.mk b/tests/Android.mk
index 4d1bfa6..3d9077d 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -23,6 +23,7 @@
 	androidx.annotation_annotation \
 	androidx.test.runner \
 	androidx.test.rules \
+	androidx.preference_preference \
 	androidx.test.uiautomator_uiautomator
 
 ifneq (,$(wildcard frameworks/base))
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index f243f27..bc6356f 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -98,7 +98,7 @@
         <activity
             android:name="com.android.launcher3.testcomponent.TestLauncherActivity"
             android:clearTaskOnLaunch="true"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
             android:enabled="false"
             android:label="Test launcher"
             android:launchMode="singleTask"
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index a1a2ed2..5e42d9b 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -53,7 +53,6 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.common.WidgetUtils;
-import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.tapl.LauncherInstrumentation;
@@ -271,8 +270,6 @@
         }
 
         mLauncherPid = 0;
-        // Disable app tracker
-        AppLaunchTracker.INSTANCE.initializeForTesting(new AppLaunchTracker());
 
         mTargetContext = InstrumentationRegistry.getTargetContext();
         mTargetPackage = mTargetContext.getPackageName();
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 34e425d..a39f215 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -41,6 +41,7 @@
 import com.android.launcher3.widget.WidgetsRecyclerView;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -101,7 +102,7 @@
         mLauncher.pressHome();
     }
 
-    @Test
+    @Ignore
     public void testOpenHomeSettingsFromWorkspace() {
         mDevice.pressMenu();
         mDevice.waitForIdle();
diff --git a/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src/com/android/launcher3/ui/WorkTabTest.java
index 8d571ff..f5f93c4 100644
--- a/tests/src/com/android/launcher3/ui/WorkTabTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkTabTest.java
@@ -24,6 +24,7 @@
 import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.util.Log;
 import android.widget.TextView;
 
 import androidx.test.filters.LargeTest;
@@ -34,6 +35,7 @@
 import com.android.launcher3.allapps.AllAppsPagedView;
 import com.android.launcher3.allapps.WorkModeSwitch;
 import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.views.WorkEduView;
 
 import org.junit.After;
@@ -132,6 +134,10 @@
                     l.getResources().getString(R.string.work_profile_edu_personal_apps));
             workEduView.findViewById(R.id.proceed).callOnClick();
         });
+
+        executeOnLauncher(launcher -> Log.d(TestProtocol.WORK_PROFILE_REMOVED,
+                "Work profile status: " + launcher.getAppsView().isPersonalTabVisible()));
+
         // verify work edu is seen next
         waitForLauncherCondition("Launcher did not show the next edu screen", l ->
                 ((AllAppsPagedView) l.getAppsView().getContentView()).getCurrentPage() == WORK_PAGE
diff --git a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index a4aa9f2..fa495f5 100644
--- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -192,7 +192,7 @@
                         WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED));
         executeOnLauncher(l -> l.getAppWidgetHost().startListening());
         verifyWidgetPresent(info);
-        assertNull(mLauncher.getWorkspace().tryGetPendingWidget(DEFAULT_UI_TIMEOUT));
+        assertNull(mLauncher.getWorkspace().tryGetPendingWidget(100));
     }
 
     @Test
diff --git a/tests/src/com/android/launcher3/util/rule/FailureInvestigator.java b/tests/src/com/android/launcher3/util/rule/FailureInvestigator.java
index e4f520f..77546de 100644
--- a/tests/src/com/android/launcher3/util/rule/FailureInvestigator.java
+++ b/tests/src/com/android/launcher3/util/rule/FailureInvestigator.java
@@ -87,12 +87,6 @@
             return 145935261;
         }
 
-        if (matches("java\\.lang\\.AssertionError\\: http\\:\\/\\/go\\/tapl \\: want to get "
-                + "workspace object; Presence of recents button doesn't match the interaction "
-                + "mode, mode\\=ZERO_BUTTON, hasRecents\\=true", exception)) {
-            return 148422894;
-        }
-
         final String logSinceBoot;
         try {
             final String systemBootTime =
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index ce94a3e..c9c846f 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -16,8 +16,9 @@
 
 package com.android.launcher3.tapl;
 
+import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
+
 import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
-import static com.android.launcher3.testing.TestProtocol.BACKGROUND_APP_STATE_ORDINAL;
 import static com.android.launcher3.testing.TestProtocol.OVERVIEW_STATE_ORDINAL;
 
 import android.graphics.Point;
@@ -85,7 +86,7 @@
                 final LauncherInstrumentation.GestureScope gestureScope =
                         zeroButtonToOverviewGestureStartsInLauncher()
                                 ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
-                                : LauncherInstrumentation.GestureScope.OUTSIDE;
+                                : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER;
 
                 // b/156044202
                 mLauncher.log("Hierarchy before swiping up to overview:");
@@ -93,7 +94,7 @@
 
                 mLauncher.sendPointer(
                         downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
-                mLauncher.executeAndWaitForEvent(
+                mLauncher.executeAndWaitForLauncherEvent(
                         () -> mLauncher.movePointer(
                                 downTime,
                                 downTime,
@@ -130,7 +131,7 @@
                 }
 
                 mLauncher.swipeToState(startX, startY, endX, endY, 10, OVERVIEW_STATE_ORDINAL,
-                        LauncherInstrumentation.GestureScope.OUTSIDE);
+                        LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER);
                 break;
             }
 
@@ -150,67 +151,68 @@
     /**
      * Swipes right or double presses the square button to switch to the previous app.
      */
+    @NonNull
     public Background quickSwitchToPreviousApp() {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "want to quick switch to the previous app")) {
             verifyActiveContainer();
-            quickSwitchToPreviousApp(getExpectedStateForQuickSwitch());
+            final boolean launcherWasVisible = mLauncher.isLauncherVisible();
+            boolean transposeInLandscape = false;
+            switch (mLauncher.getNavigationModel()) {
+                case TWO_BUTTON:
+                    transposeInLandscape = true;
+                    // Fall through, zero button and two button modes behave the same.
+                case ZERO_BUTTON: {
+                    final int startX;
+                    final int startY;
+                    final int endX;
+                    final int endY;
+                    if (mLauncher.getDevice().isNaturalOrientation() || !transposeInLandscape) {
+                        // Swipe from the bottom left to the bottom right of the screen.
+                        startX = 0;
+                        startY = getSwipeStartY();
+                        endX = mLauncher.getDevice().getDisplayWidth();
+                        endY = startY;
+                    } else {
+                        // Swipe from the bottom right to the top right of the screen.
+                        startX = getSwipeStartX();
+                        startY = mLauncher.getRealDisplaySize().y - 1;
+                        endX = startX;
+                        endY = 0;
+                    }
+                    final boolean isZeroButton = mLauncher.getNavigationModel()
+                            == LauncherInstrumentation.NavigationModel.ZERO_BUTTON;
+                    LauncherInstrumentation.GestureScope gestureScope =
+                            launcherWasVisible && isZeroButton
+                                    ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
+                                    : LauncherInstrumentation.GestureScope.OUTSIDE_WITH_PILFER;
+                    mLauncher.executeAndWaitForEvent(
+                            () -> mLauncher.linearGesture(
+                                    startX, startY, endX, endY, 20, false, gestureScope),
+                            event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
+                            () -> "Quick switch gesture didn't change window state");
+                    break;
+                }
+
+                case THREE_BUTTON:
+                    // Double press the recents button.
+                    UiObject2 recentsButton = mLauncher.waitForSystemUiObject("recent_apps");
+                    mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+                    mLauncher.runToState(() -> recentsButton.click(), OVERVIEW_STATE_ORDINAL);
+                    mLauncher.getOverview();
+                    mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
+                    mLauncher.executeAndWaitForEvent(
+                            () -> recentsButton.click(),
+                            event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
+                            () -> "Pressing recents button didn't change window state");
+                    break;
+            }
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
             return new Background(mLauncher);
         }
     }
 
-    protected int getExpectedStateForQuickSwitch() {
-        return BACKGROUND_APP_STATE_ORDINAL;
-    }
-
-    protected void quickSwitchToPreviousApp(int expectedState) {
-        final boolean launcherWasVisible = mLauncher.isLauncherVisible();
-        boolean transposeInLandscape = false;
-        switch (mLauncher.getNavigationModel()) {
-            case TWO_BUTTON:
-                transposeInLandscape = true;
-                // Fall through, zero button and two button modes behave the same.
-            case ZERO_BUTTON: {
-                final int startX;
-                final int startY;
-                final int endX;
-                final int endY;
-                if (mLauncher.getDevice().isNaturalOrientation() || !transposeInLandscape) {
-                    // Swipe from the bottom left to the bottom right of the screen.
-                    startX = 0;
-                    startY = getSwipeStartY();
-                    endX = mLauncher.getDevice().getDisplayWidth();
-                    endY = startY;
-                } else {
-                    // Swipe from the bottom right to the top right of the screen.
-                    startX = getSwipeStartX();
-                    startY = mLauncher.getRealDisplaySize().y - 1;
-                    endX = startX;
-                    endY = 0;
-                }
-                final boolean isZeroButton = mLauncher.getNavigationModel()
-                        == LauncherInstrumentation.NavigationModel.ZERO_BUTTON;
-                mLauncher.swipeToState(startX, startY, endX, endY, 20, expectedState,
-                        launcherWasVisible && isZeroButton
-                                ? LauncherInstrumentation.GestureScope.INSIDE_TO_OUTSIDE
-                                : LauncherInstrumentation.GestureScope.OUTSIDE);
-                break;
-            }
-
-            case THREE_BUTTON:
-                // Double press the recents button.
-                UiObject2 recentsButton = mLauncher.waitForSystemUiObject("recent_apps");
-                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
-                mLauncher.runToState(() -> recentsButton.click(), OVERVIEW_STATE_ORDINAL);
-                mLauncher.getOverview();
-                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
-                recentsButton.click();
-                break;
-        }
-        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
-    }
-
     protected String getSwipeHeightRequestName() {
         return TestProtocol.REQUEST_BACKGROUND_TO_OVERVIEW_SWIPE_HEIGHT;
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 223ae29..588b6b8 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -27,7 +27,7 @@
 import java.util.List;
 
 /**
- * Common overview pane for both Launcher and fallback recents
+ * Common overview panel for both Launcher and fallback recents
  */
 public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
     private static final int FLINGS_FOR_DISMISS_LIMIT = 40;
@@ -135,4 +135,19 @@
     public boolean hasTasks() {
         return getTasks().size() > 0;
     }
+
+    /**
+     * Gets Overview Actions.
+     *
+     * @return The Overview Actions
+     */
+    @NonNull
+    public OverviewActions getOverviewActions() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to get overview actions")) {
+            verifyActiveContainer();
+            UiObject2 overviewActions = mLauncher.waitForLauncherObject("action_buttons");
+            return new OverviewActions(overviewActions, mLauncher);
+        }
+    }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/Home.java b/tests/tapl/com/android/launcher3/tapl/Home.java
index c06e254..0060844 100644
--- a/tests/tapl/com/android/launcher3/tapl/Home.java
+++ b/tests/tapl/com/android/launcher3/tapl/Home.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3.tapl;
 
-import static com.android.launcher3.testing.TestProtocol.QUICK_SWITCH_STATE_ORDINAL;
-
 import androidx.annotation.NonNull;
 
 /**
@@ -65,8 +63,4 @@
         return true;
     }
 
-    @Override
-    protected int getExpectedStateForQuickSwitch() {
-        return QUICK_SWITCH_STATE_ORDINAL;
-    }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 13ecfb8..093c024 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -56,15 +56,18 @@
     private Background launch(BySelector selector) {
         LauncherInstrumentation.log("Launchable.launch before click " +
                 mObject.getVisibleCenter() + " in " + mLauncher.getVisibleBounds(mObject));
+        final String label = mObject.getText();
 
         mLauncher.executeAndWaitForEvent(
-                () -> mLauncher.clickLauncherObject(mObject),
+                () -> {
+                    mLauncher.clickLauncherObject(mObject);
+                    expectActivityStartEvents();
+                },
                 event -> event.getEventType() == TYPE_WINDOW_STATE_CHANGED,
-                () -> "Launching an app didn't open a new window: " + mObject.getText());
-        expectActivityStartEvents();
+                () -> "Launching an app didn't open a new window: " + label);
 
         mLauncher.assertTrue(
-                "App didn't start: " + selector,
+                "App didn't start: " + label,
                 mLauncher.getDevice().wait(Until.hasObject(selector),
                         LauncherInstrumentation.WAIT_TIME_MS));
         return new Background(mLauncher);
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 53d60a0..c621616 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -103,6 +103,7 @@
 
     static final Pattern EVENT_TOUCH_DOWN_TIS = getTouchEventPatternTIS("ACTION_DOWN");
     static final Pattern EVENT_TOUCH_UP_TIS = getTouchEventPatternTIS("ACTION_UP");
+    private final String mLauncherPackage;
 
     // Types for launcher containers that the user is interacting with. "Background" is a
     // pseudo-container corresponding to inactive launcher covered by another app.
@@ -112,9 +113,10 @@
 
     public enum NavigationModel {ZERO_BUTTON, TWO_BUTTON, THREE_BUTTON}
 
-    // Where the gesture happens: outside of Launcher, inside or from inside to outside.
+    // Where the gesture happens: outside of Launcher, inside or from inside to outside and
+    // whether the gesture recognition triggers pilfer.
     public enum GestureScope {
-        OUTSIDE, INSIDE, INSIDE_TO_OUTSIDE
+        OUTSIDE_WITHOUT_PILFER, OUTSIDE_WITH_PILFER, INSIDE, INSIDE_TO_OUTSIDE
     }
 
     ;
@@ -154,6 +156,7 @@
     public static final int WAIT_TIME_MS = 10000;
     public static final int LONG_WAIT_TIME_MS = 60000;
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+    private static final String ANDROID_PACKAGE = "android";
 
     private static WeakReference<VisibleContainer> sActiveContainer = new WeakReference<>(null);
 
@@ -215,11 +218,11 @@
         // Launcher package. As during inproc tests the tested launcher may not be selected as the
         // current launcher, choosing target package for inproc. For out-of-proc, use the installed
         // launcher package.
-        final String authorityPackage = testPackage.equals(targetPackage) ?
-                getLauncherPackageName() :
-                targetPackage;
+        mLauncherPackage = testPackage.equals(targetPackage)
+                ? getLauncherPackageName()
+                : targetPackage;
 
-        String testProviderAuthority = authorityPackage + ".TestInfo";
+        String testProviderAuthority = mLauncherPackage + ".TestInfo";
         mTestProviderUri = new Uri.Builder()
                 .scheme(ContentResolver.SCHEME_CONTENT)
                 .authority(testProviderAuthority)
@@ -449,7 +452,7 @@
             }
         }
 
-        dumpDiagnostics();
+        dumpDiagnostics(message);
 
         log("Hierarchy dump for: " + message);
         dumpViewHierarchy();
@@ -457,10 +460,11 @@
         return message;
     }
 
-    private void dumpDiagnostics() {
-        Log.e("b/156287114", "Input:");
+    private void dumpDiagnostics(String message) {
+        log("Diagnostics for failure: " + message);
+        log("Input:");
         logShellCommand("dumpsys input");
-        Log.e("b/156287114", "TIS:");
+        log("TIS:");
         logShellCommand("dumpsys activity service TouchInteractionService");
     }
 
@@ -468,10 +472,10 @@
         try {
             for (String line : mDevice.executeShellCommand(command).split("\\n")) {
                 SystemClock.sleep(10);
-                Log.d("b/156287114", line);
+                log(line);
             }
         } catch (IOException e) {
-            Log.d("b/156287114", "Failed to execute " + command);
+            log("Failed to execute " + command);
         }
     }
 
@@ -549,13 +553,7 @@
         assertEquals("Unexpected display rotation",
                 mExpectedRotation, mDevice.getDisplayRotation());
 
-        // b/148422894
-        String error = null;
-        for (int i = 0; i != 600; ++i) {
-            error = getNavigationModeMismatchError();
-            if (error == null) break;
-            sleep(100);
-        }
+        final String error = getNavigationModeMismatchError();
         assertTrue(error, error == null);
 
         log("verifyContainerType: " + containerType);
@@ -633,14 +631,20 @@
         fail("Launcher didn't initialize");
     }
 
+    Parcelable executeAndWaitForLauncherEvent(Runnable command,
+            UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message) {
+        return executeAndWaitForEvent(
+                command,
+                e -> mLauncherPackage.equals(e.getPackageName()) && eventFilter.accept(e),
+                message);
+    }
+
     Parcelable executeAndWaitForEvent(Runnable command,
             UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message) {
         try {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "executeAndWaitForEvent: before");
             final AccessibilityEvent event =
                     mInstrumentation.getUiAutomation().executeAndWaitForEvent(
                             command, eventFilter, LONG_WAIT_TIME_MS);
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "executeAndWaitForEvent: after");
             assertNotNull("executeAndWaitForEvent returned null (this can't happen)", event);
             final Parcelable parcelableData = event.getParcelableData();
             event.recycle();
@@ -696,7 +700,7 @@
                                 ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL,
                                 launcherWasVisible
                                         ? GestureScope.INSIDE_TO_OUTSIDE
-                                        : GestureScope.OUTSIDE);
+                                        : GestureScope.OUTSIDE_WITH_PILFER);
                     }
                 }
             } else {
@@ -928,6 +932,14 @@
         return waitForObjectBySelector(getOverviewObjectSelector(resName));
     }
 
+    @NonNull
+    UiObject2 waitForAndroidObject(String resId) {
+        final UiObject2 object = mDevice.wait(
+                Until.findObject(By.res(ANDROID_PACKAGE, resId)), WAIT_TIME_MS);
+        assertNotNull("Can't find a android object with id: " + resId, object);
+        return object;
+    }
+
     private UiObject2 waitForObjectBySelector(BySelector selector) {
         final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
         assertNotNull("Can't find a view in Launcher, selector: " + selector, object);
@@ -979,7 +991,7 @@
 
     void runToState(Runnable command, int expectedState) {
         final List<Integer> actualEvents = new ArrayList<>();
-        executeAndWaitForEvent(
+        executeAndWaitForLauncherEvent(
                 command,
                 event -> isSwitchToStateEvent(event, expectedState, actualEvents),
                 () -> "Failed to receive an event for the state change: expected ["
@@ -1094,13 +1106,10 @@
                 return;
         }
 
-        executeAndWaitForEvent(
+        executeAndWaitForLauncherEvent(
                 () -> linearGesture(
                         startX, startY, endX, endY, steps, slowDown, GestureScope.INSIDE),
-                event -> {
-                    Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "scroll: received event: " + event);
-                    return TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName());
-                },
+                event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
                 () -> "Didn't receive a scroll end message: " + startX + ", " + startY
                         + ", " + endX + ", " + endY);
     }
@@ -1165,7 +1174,8 @@
         final boolean notLauncher3 = !isLauncher3();
         switch (action) {
             case MotionEvent.ACTION_DOWN:
-                if (gestureScope != GestureScope.OUTSIDE) {
+                if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
+                        && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER) {
                     expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN);
                 }
                 if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
@@ -1173,12 +1183,17 @@
                 }
                 break;
             case MotionEvent.ACTION_UP:
-                if (notLauncher3 && gestureScope != GestureScope.INSIDE) {
+                if (notLauncher3 && gestureScope != GestureScope.INSIDE
+                        && (gestureScope == GestureScope.OUTSIDE_WITH_PILFER
+                        || gestureScope == GestureScope.INSIDE_TO_OUTSIDE)) {
                     expectEvent(TestProtocol.SEQUENCE_PILFER, EVENT_PILFER_POINTERS);
                 }
-                if (gestureScope != GestureScope.OUTSIDE) {
-                    expectEvent(TestProtocol.SEQUENCE_MAIN, gestureScope == GestureScope.INSIDE
-                            ? EVENT_TOUCH_UP : EVENT_TOUCH_CANCEL);
+                if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
+                        && gestureScope != GestureScope.OUTSIDE_WITHOUT_PILFER) {
+                    expectEvent(TestProtocol.SEQUENCE_MAIN,
+                            gestureScope == GestureScope.INSIDE
+                                    || gestureScope == GestureScope.OUTSIDE_WITHOUT_PILFER
+                                    ? EVENT_TOUCH_UP : EVENT_TOUCH_CANCEL);
                 }
                 if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
                     expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
@@ -1301,6 +1316,11 @@
                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    boolean overviewShareEnabled() {
+        return getTestInfo(TestProtocol.REQUEST_OVERVIEW_SHARE_ENABLED).getBoolean(
+                TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
+
     private void disableSensorRotation() {
         getTestInfo(TestProtocol.REQUEST_MOCK_SENSOR_ROTATION);
     }
@@ -1366,7 +1386,7 @@
                 if (mCheckEventsForSuccessfulGestures) {
                     final String message = eventChecker.verify(WAIT_TIME_MS, true);
                     if (message != null) {
-                        dumpDiagnostics();
+                        dumpDiagnostics(message);
                         checkForAnomaly();
                         Assert.fail(formatSystemHealthMessage(
                                 "http://go/tapl : successful gesture produced " + message));
@@ -1398,4 +1418,4 @@
             return null;
         }
     }
-}
+}
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewActions.java b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
new file mode 100644
index 0000000..a30a404
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
@@ -0,0 +1,130 @@
+/*
+ * 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.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.UiObject2;
+
+import com.android.launcher3.testing.TestProtocol;
+
+/**
+ * View containing overview actions
+ */
+public class OverviewActions {
+    private final UiObject2 mOverviewActions;
+    private final LauncherInstrumentation mLauncher;
+
+    OverviewActions(UiObject2 overviewActions, LauncherInstrumentation launcherInstrumentation) {
+        this.mOverviewActions = overviewActions;
+        this.mLauncher = launcherInstrumentation;
+    }
+
+    /**
+     * Clicks screenshot button and closes screenshot ui.
+     */
+    @NonNull
+    public Overview clickAndDismissScreenshot() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to click screenshot button and exit screenshot ui")) {
+            UiObject2 screenshot = mLauncher.waitForObjectInContainer(mOverviewActions,
+                    "action_screenshot");
+            mLauncher.clickLauncherObject(screenshot);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked screenshot button")) {
+                UiObject2 closeScreenshot = mLauncher.waitForSystemUiObject(
+                        "global_screenshot_dismiss_image");
+                if (mLauncher.getNavigationModel()
+                        != LauncherInstrumentation.NavigationModel.THREE_BUTTON) {
+                    mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS,
+                            LauncherInstrumentation.EVENT_TOUCH_DOWN_TIS);
+                    mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS,
+                            LauncherInstrumentation.EVENT_TOUCH_UP_TIS);
+                }
+                closeScreenshot.click();
+                try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                        "dismissed screenshot")) {
+                    return new Overview(mLauncher);
+                }
+            }
+        }
+    }
+
+    /**
+     * Click share button, then drags sharesheet down to remove it.
+     *
+     * Share is currently hidden behind flag, test is kept in case share becomes a default feature.
+     * If share is completely removed then remove this test as well.
+     */
+    @NonNull
+    public Overview clickAndDismissShare() {
+        if (mLauncher.overviewShareEnabled()) {
+            try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+                 LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                         "want to click share button and dismiss sharesheet")) {
+                UiObject2 share = mLauncher.waitForObjectInContainer(mOverviewActions,
+                        "action_share");
+                mLauncher.clickLauncherObject(share);
+                try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                        "clicked share button")) {
+                    mLauncher.waitForAndroidObject("contentPanel");
+                    mLauncher.getDevice().pressBack();
+                    try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                            "dismissed sharesheet")) {
+                        return new Overview(mLauncher);
+                    }
+                }
+            }
+        }
+        return new Overview(mLauncher);
+    }
+
+    /**
+     * Click select button
+     *
+     * @return The select mode buttons that are now shown instead of action buttons.
+     */
+    @NonNull
+    public SelectModeButtons clickSelect() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
+                     mLauncher.addContextLayer("want to click select button")) {
+            UiObject2 select = mLauncher.waitForObjectInContainer(mOverviewActions,
+                    "action_select");
+            mLauncher.clickLauncherObject(select);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked select button")) {
+                return getSelectModeButtons();
+            }
+
+        }
+    }
+
+    /**
+     * Gets the Select Mode Buttons.
+     *
+     * @return The Select Mode Buttons.
+     */
+    @NonNull
+    private SelectModeButtons getSelectModeButtons() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to get select mode buttons")) {
+            UiObject2 selectModeButtons = mLauncher.waitForLauncherObject("select_mode_buttons");
+            return new SelectModeButtons(selectModeButtons, mLauncher);
+        }
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java
new file mode 100644
index 0000000..3507418
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java
@@ -0,0 +1,68 @@
+/*
+ * 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.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.UiObject2;
+
+/**
+ * View containing select mode buttons
+ */
+public class SelectModeButtons {
+    private final UiObject2 mSelectModeButtons;
+    private final LauncherInstrumentation mLauncher;
+
+    SelectModeButtons(UiObject2 selectModeButtons,
+            LauncherInstrumentation launcherInstrumentation) {
+        mSelectModeButtons = selectModeButtons;
+        mLauncher = launcherInstrumentation;
+    }
+
+    /**
+     * Click close button.
+     */
+    @NonNull
+    public Overview clickClose() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
+                     mLauncher.addContextLayer("want to click close button")) {
+            UiObject2 close = mLauncher.waitForObjectInContainer(mSelectModeButtons, "close");
+            mLauncher.clickLauncherObject(close);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked close button")) {
+                return new Overview(mLauncher);
+            }
+        }
+    }
+
+    /**
+     * Click feedback button.
+     */
+    @NonNull
+    public Background clickFeedback() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
+                     mLauncher.addContextLayer("want to click feedback button")) {
+            UiObject2 feedback = mLauncher.waitForObjectInContainer(mSelectModeButtons, "feedback");
+            mLauncher.clickLauncherObject(feedback);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked feedback button")) {
+                return new Background(mLauncher);
+            }
+        }
+    }
+}