Merge "Refactor / Cleanup / Simplify a bunch of dnd related rendering / animation" into ub-launcher3-master
diff --git a/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml b/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml
index b0f2b4b..980bb5a 100644
--- a/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml
+++ b/quickstep/recents_ui_overrides/res/layout/arrow_toast.xml
@@ -34,6 +34,7 @@
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_weight="1"
+            android:gravity="center"
             android:layout_gravity="center_vertical"
             android:textColor="@android:color/white"
             android:textSize="16sp"/>
diff --git a/quickstep/recents_ui_overrides/res/values/config.xml b/quickstep/recents_ui_overrides/res/values/config.xml
index 527eec6..120e034 100644
--- a/quickstep/recents_ui_overrides/res/values/config.xml
+++ b/quickstep/recents_ui_overrides/res/values/config.xml
@@ -14,8 +14,5 @@
      limitations under the License.
 -->
 <resources>
-    <integer name="app_background_blur_radius">150</integer>
-    <integer name="allapps_background_blur_radius">90</integer>
-    <integer name="overview_background_blur_radius">50</integer>
-    <integer name="folder_background_blur_radius_adjustment">20</integer>
+    <integer name="max_depth_blur_radius">150</integer>
 </resources>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/ArrowTipView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/ArrowTipView.java
new file mode 100644
index 0000000..a5ea523
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/ArrowTipView.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2008 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 android.content.Context;
+import android.graphics.CornerPathEffect;
+import android.graphics.Paint;
+import android.graphics.drawable.ShapeDrawable;
+import android.os.Handler;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.core.content.ContextCompat;
+
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.graphics.TriangleShape;
+
+/**
+ * A base class for arrow tip view in launcher
+ */
+public class ArrowTipView extends AbstractFloatingView {
+
+    private static final long AUTO_CLOSE_TIMEOUT_MILLIS = 10 * 1000;
+    private static final long SHOW_DELAY_MS = 200;
+    private static final long SHOW_DURATION_MS = 300;
+    private static final long HIDE_DURATION_MS = 100;
+
+    protected final Launcher mLauncher;
+    private final Handler mHandler = new Handler();
+    private Runnable mOnClosed;
+
+    public ArrowTipView(Context context) {
+        super(context, null, 0);
+        mLauncher = Launcher.getLauncher(context);
+        init(context);
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            close(true);
+        }
+        return false;
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        if (mIsOpen) {
+            if (animate) {
+                animate().alpha(0f)
+                        .withLayer()
+                        .setStartDelay(0)
+                        .setDuration(HIDE_DURATION_MS)
+                        .setInterpolator(Interpolators.ACCEL)
+                        .withEndAction(() -> mLauncher.getDragLayer().removeView(this))
+                        .start();
+            } else {
+                animate().cancel();
+                mLauncher.getDragLayer().removeView(this);
+            }
+            if (mOnClosed != null) mOnClosed.run();
+            mIsOpen = false;
+        }
+    }
+
+    @Override
+    public void logActionCommand(int command) {
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_ON_BOARD_POPUP) != 0;
+    }
+
+    private void init(Context context) {
+        inflate(context, R.layout.arrow_toast, this);
+        setOrientation(LinearLayout.VERTICAL);
+        View dismissButton = findViewById(R.id.dismiss);
+        dismissButton.setOnClickListener(view -> {
+            handleClose(true);
+        });
+
+        View arrowView = findViewById(R.id.arrow);
+        ViewGroup.LayoutParams arrowLp = arrowView.getLayoutParams();
+        ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
+                arrowLp.width, arrowLp.height, false));
+        Paint arrowPaint = arrowDrawable.getPaint();
+        TypedValue typedValue = new TypedValue();
+        context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
+        arrowPaint.setColor(ContextCompat.getColor(getContext(), typedValue.resourceId));
+        // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
+        arrowPaint.setPathEffect(new CornerPathEffect(
+                context.getResources().getDimension(R.dimen.arrow_toast_corner_radius)));
+        arrowView.setBackground(arrowDrawable);
+
+        mIsOpen = true;
+
+        mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
+    }
+
+    /**
+     * Show Tip with specified string and Y location
+     */
+    public ArrowTipView show(String text, int top) {
+        ((TextView) findViewById(R.id.text)).setText(text);
+        mLauncher.getDragLayer().addView(this);
+
+        DragLayer.LayoutParams params = (DragLayer.LayoutParams) getLayoutParams();
+        params.gravity = Gravity.CENTER_HORIZONTAL;
+        params.leftMargin = mLauncher.getDeviceProfile().workspacePadding.left;
+        params.rightMargin = mLauncher.getDeviceProfile().workspacePadding.right;
+        post(() -> setY(top - getHeight()));
+        setAlpha(0);
+        animate()
+                .alpha(1f)
+                .withLayer()
+                .setStartDelay(SHOW_DELAY_MS)
+                .setDuration(SHOW_DURATION_MS)
+                .setInterpolator(Interpolators.DEACCEL)
+                .start();
+        return this;
+    }
+
+    /**
+     * Register a callback fired when toast is hidden
+     */
+    public ArrowTipView setOnClosedCallback(Runnable runnable) {
+        mOnClosed = runnable;
+        return this;
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index 9afa862..0019ecb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -91,7 +91,7 @@
         AppWindowAnimationHelper helper =
             new AppWindowAnimationHelper(recentsView.getPagedViewOrientedState(), mLauncher);
         Animator recentsAnimator = getRecentsWindowAnimator(taskView, skipLauncherChanges,
-                appTargets, wallpaperTargets, mLauncher.getBackgroundBlurController(), helper);
+                appTargets, wallpaperTargets, mLauncher.getDepthController(), helper);
         anim.play(recentsAnimator.setDuration(RECENTS_LAUNCH_DURATION));
 
         Animator childStateAnimation = null;
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
index 0ae7435..b3bb850 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/AllAppsTipView.java
@@ -16,133 +16,30 @@
 
 package com.android.launcher3.appprediction;
 
+import static com.android.launcher3.AbstractFloatingView.TYPE_DISCOVERY_BOUNCE;
+import static com.android.launcher3.AbstractFloatingView.TYPE_ON_BOARD_POPUP;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.quickstep.logging.UserEventDispatcherExtension.ALL_APPS_PREDICTION_TIPS;
 
-import android.content.Context;
-import android.graphics.CornerPathEffect;
-import android.graphics.Paint;
-import android.graphics.drawable.ShapeDrawable;
-import android.os.Handler;
 import android.os.UserManager;
-import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.core.content.ContextCompat;
 
 import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.ArrowTipView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.FloatingHeaderView;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.graphics.TriangleShape;
 import com.android.systemui.shared.system.LauncherEventUtil;
 
 /**
- * All apps tip view aligned just above prediction apps, shown to users that enter all apps for the
+ * ArrowTip helper aligned just above prediction apps, shown to users that enter all apps for the
  * first time.
  */
-public class AllAppsTipView extends AbstractFloatingView {
+public class AllAppsTipView {
 
     private static final String ALL_APPS_TIP_SEEN = "launcher.all_apps_tip_seen";
-    private static final long AUTO_CLOSE_TIMEOUT_MILLIS = 10 * 1000;
-    private static final long SHOW_DELAY_MS = 200;
-    private static final long SHOW_DURATION_MS = 300;
-    private static final long HIDE_DURATION_MS = 100;
-
-    private final Launcher mLauncher;
-    private final Handler mHandler = new Handler();
-
-    private AllAppsTipView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    private AllAppsTipView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        setOrientation(LinearLayout.VERTICAL);
-
-        mLauncher = Launcher.getLauncher(context);
-
-        init(context);
-    }
-
-    @Override
-    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            close(true);
-        }
-        return false;
-    }
-
-    @Override
-    protected void handleClose(boolean animate) {
-        if (mIsOpen) {
-            if (animate) {
-                animate().alpha(0f)
-                        .withLayer()
-                        .setStartDelay(0)
-                        .setDuration(HIDE_DURATION_MS)
-                        .setInterpolator(Interpolators.ACCEL)
-                        .withEndAction(() -> mLauncher.getDragLayer().removeView(this))
-                        .start();
-            } else {
-                animate().cancel();
-                mLauncher.getDragLayer().removeView(this);
-            }
-            mLauncher.getSharedPrefs().edit().putBoolean(ALL_APPS_TIP_SEEN, true).apply();
-            mIsOpen = false;
-        }
-    }
-
-    @Override
-    public void logActionCommand(int command) {
-    }
-
-    @Override
-    protected boolean isOfType(int type) {
-        return (type & TYPE_ON_BOARD_POPUP) != 0;
-    }
-
-    private void init(Context context) {
-        inflate(context, R.layout.arrow_toast, this);
-
-        TextView textView = findViewById(R.id.text);
-        textView.setText(R.string.all_apps_prediction_tip);
-
-        View dismissButton = findViewById(R.id.dismiss);
-        dismissButton.setOnClickListener(view -> {
-            mLauncher.getUserEventDispatcher().logActionTip(
-                    LauncherEventUtil.DISMISS, ALL_APPS_PREDICTION_TIPS);
-            handleClose(true);
-        });
-
-        View arrowView = findViewById(R.id.arrow);
-        ViewGroup.LayoutParams arrowLp = arrowView.getLayoutParams();
-        ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
-                arrowLp.width, arrowLp.height, false));
-        Paint arrowPaint = arrowDrawable.getPaint();
-        TypedValue typedValue = new TypedValue();
-        context.getTheme().resolveAttribute(android.R.attr.colorAccent, typedValue, true);
-        arrowPaint.setColor(ContextCompat.getColor(getContext(), typedValue.resourceId));
-        // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
-        arrowPaint.setPathEffect(new CornerPathEffect(
-                context.getResources().getDimension(R.dimen.arrow_toast_corner_radius)));
-        arrowView.setBackground(arrowDrawable);
-
-        mIsOpen = true;
-
-        mHandler.postDelayed(() -> handleClose(true), AUTO_CLOSE_TIMEOUT_MILLIS);
-    }
 
     private static boolean showAllAppsTipIfNecessary(Launcher launcher) {
         FloatingHeaderView floatingHeaderView = launcher.getAppsView().getFloatingHeaderView();
@@ -156,28 +53,15 @@
             return false;
         }
 
-        AllAppsTipView allAppsTipView = new AllAppsTipView(launcher.getAppsView().getContext(),
-            null);
-        launcher.getDragLayer().addView(allAppsTipView);
+        int[] coords = new int[2];
+        floatingHeaderView.findFixedRowByType(PredictionRowView.class).getLocationOnScreen(coords);
+        ArrowTipView arrowTipView = new ArrowTipView(launcher).setOnClosedCallback(() -> {
+            launcher.getSharedPrefs().edit().putBoolean(ALL_APPS_TIP_SEEN, true).apply();
+            launcher.getUserEventDispatcher().logActionTip(LauncherEventUtil.DISMISS,
+                    ALL_APPS_PREDICTION_TIPS);
+        });
+        arrowTipView.show(launcher.getString(R.string.all_apps_prediction_tip), coords[1]);
 
-        DragLayer.LayoutParams params = (DragLayer.LayoutParams) allAppsTipView.getLayoutParams();
-        params.gravity = Gravity.CENTER_HORIZONTAL;
-
-        int top = floatingHeaderView.findFixedRowByType(PredictionRowView.class).getTop();
-        allAppsTipView.setY(top - launcher.getResources().getDimensionPixelSize(
-                R.dimen.all_apps_tip_bottom_margin));
-
-        allAppsTipView.setAlpha(0);
-        allAppsTipView.animate()
-                .alpha(1f)
-                .withLayer()
-                .setStartDelay(SHOW_DELAY_MS)
-                .setDuration(SHOW_DURATION_MS)
-                .setInterpolator(Interpolators.DEACCEL)
-                .start();
-
-        launcher.getUserEventDispatcher().logActionTip(
-                LauncherEventUtil.VISIBLE, ALL_APPS_PREDICTION_TIPS);
         return true;
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
index a07cd1d..da58817 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
@@ -23,23 +23,28 @@
 import android.content.res.Configuration;
 import android.os.Build;
 import android.view.View;
-import android.view.ViewGroup;
 
 import androidx.core.app.NotificationCompat;
 
 import com.android.launcher3.CellLayout;
+import com.android.launcher3.FolderInfo;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.WorkspaceLayoutManager;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.ActivityTracker;
+import com.android.launcher3.util.GridOccupancy;
+import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.Themes;
 
+import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -52,48 +57,179 @@
     private static final int ONBOARDING_NOTIFICATION_ID = 7641;
 
     private final Launcher mLauncher;
+    private final NotificationManager mNotificationManager;
+    private final Notification mNotification;
     private List<WorkspaceItemInfo> mPredictedApps;
     private HotseatEduDialog mActiveDialog;
 
-    private final NotificationManager mNotificationManager;
-    private final Notification mNotification;
+    private ArrayList<ItemInfo> mNewItems = new ArrayList<>();
+    private IntArray mNewScreens = null;
+    private Runnable mOnOnboardingComplete;
 
-    HotseatEduController(Launcher launcher) {
+    HotseatEduController(Launcher launcher, Runnable runnable) {
         mLauncher = launcher;
+        mOnOnboardingComplete = runnable;
         mNotificationManager = mLauncher.getSystemService(NotificationManager.class);
         createNotificationChannel();
         mNotification = createNotification();
     }
 
-    boolean migrate() {
-        Workspace workspace = mLauncher.getWorkspace();
-        CellLayout firstScreen = workspace.getScreenWithId(WorkspaceLayoutManager.FIRST_SCREEN_ID);
-        int toPage = Workspace.FIRST_SCREEN_ID;
-        int toRow = mLauncher.getDeviceProfile().inv.numRows - 1;
-        if (FeatureFlags.HOTSEAT_MIGRATE_NEW_PAGE.get()) {
-            toPage = workspace.getScreenIdForPageIndex(workspace.getPageCount());
-            toRow = 0;
-        } else if (!firstScreen.makeSpaceForHotseatMigration(true)) {
-            return false;
+    /**
+     * Checks what type of migration should be used and migrates hotseat
+     */
+    void migrate() {
+        if (FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()) {
+            migrateToFolder();
+        } else {
+            migrateHotseatWhole();
         }
-        ViewGroup hotseatVG = mLauncher.getHotseat().getShortcutsAndWidgets();
-        for (int i = 0; i < hotseatVG.getChildCount(); i++) {
-            View child = hotseatVG.getChildAt(i);
+    }
+
+    /**
+     * This migration places all non folder items in the hotseat into a folder and then moves
+     * all folders in the hotseat to a workspace page that has enough empty spots.
+     *
+     * @return pageId that has accepted the items.
+     */
+    private int migrateToFolder() {
+        ArrayDeque<FolderInfo> folders = new ArrayDeque<>();
+
+        ArrayList<WorkspaceItemInfo> putIntoFolder = new ArrayList<>();
+
+        //separate folders and items that can get in folders
+        for (int i = 0; i < mLauncher.getDeviceProfile().inv.numHotseatIcons; i++) {
+            View view = mLauncher.getHotseat().getChildAt(i, 0);
+            if (view == null) continue;
+            ItemInfo info = (ItemInfo) view.getTag();
+            if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+                folders.add((FolderInfo) info);
+            } else if (info instanceof WorkspaceItemInfo) {
+                putIntoFolder.add((WorkspaceItemInfo) info);
+            }
+        }
+
+        // create a temp folder and add non folder items to it
+        if (!putIntoFolder.isEmpty()) {
+            ItemInfo firstItem = putIntoFolder.get(0);
+            FolderInfo folderInfo = new FolderInfo();
+            folderInfo.title = "";
+            mLauncher.getModelWriter().addItemToDatabase(folderInfo, firstItem.container,
+                    firstItem.screenId, firstItem.cellX, firstItem.cellY);
+            folderInfo.contents.addAll(putIntoFolder);
+            for (int i = 0; i < folderInfo.contents.size(); i++) {
+                ItemInfo item = folderInfo.contents.get(i);
+                item.rank = i;
+                mLauncher.getModelWriter().moveItemInDatabase(item, folderInfo.id, 0,
+                        item.cellX, item.cellY);
+            }
+            folders.add(folderInfo);
+        }
+        mNewItems.addAll(folders);
+
+        return placeFoldersInWorkspace(folders);
+    }
+
+    private int placeFoldersInWorkspace(ArrayDeque<FolderInfo> folders) {
+        if (folders.isEmpty()) return 0;
+
+        Workspace workspace = mLauncher.getWorkspace();
+        InvariantDeviceProfile idp = mLauncher.getDeviceProfile().inv;
+
+        GridOccupancy[] occupancyList = new GridOccupancy[workspace.getChildCount()];
+        for (int i = 0; i < occupancyList.length; i++) {
+            occupancyList[i] = ((CellLayout) workspace.getChildAt(i)).cloneGridOccupancy();
+        }
+        //scan every screen to find available spots to place folders
+        int occupancyIndex = 0;
+        int[] itemXY = new int[2];
+        while (occupancyIndex < occupancyList.length && !folders.isEmpty()) {
+            GridOccupancy occupancy = occupancyList[occupancyIndex];
+            if (occupancy.findVacantCell(itemXY, 1, 1)) {
+                FolderInfo info = folders.poll();
+                mLauncher.getModelWriter().moveItemInDatabase(info,
+                        LauncherSettings.Favorites.CONTAINER_DESKTOP,
+                        workspace.getScreenIdForPageIndex(occupancyIndex), itemXY[0], itemXY[1]);
+                occupancy.markCells(info, true);
+            } else {
+                occupancyIndex++;
+            }
+        }
+        if (folders.isEmpty()) return workspace.getScreenIdForPageIndex(occupancyIndex);
+        int screenId = LauncherSettings.Settings.call(mLauncher.getContentResolver(),
+                LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
+                .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+        // if all screens are full and we still have folders left, put those on a new page
+        FolderInfo folderInfo;
+        int col = 0;
+        while ((folderInfo = folders.poll()) != null) {
+            mLauncher.getModelWriter().moveItemInDatabase(folderInfo,
+                    LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, col++,
+                    idp.numRows - 1);
+        }
+        mNewScreens = IntArray.wrap(screenId);
+        return workspace.getPageCount();
+    }
+
+    /**
+     * This migration option attempts to move the entire hotseat up to the first workspace that
+     * has space to host items. If no such page is found, it moves items to a new page.
+     *
+     * @return pageId where items are migrated
+     */
+    private int migrateHotseatWhole() {
+        Workspace workspace = mLauncher.getWorkspace();
+        Hotseat hotseatVG = mLauncher.getHotseat();
+
+        int pageId = -1;
+        int toRow = 0;
+        for (int i = 0; i < workspace.getPageCount(); i++) {
+            CellLayout target = workspace.getScreenWithId(workspace.getScreenIdForPageIndex(i));
+            if (target.makeSpaceForHotseatMigration(true)) {
+                toRow = mLauncher.getDeviceProfile().inv.numRows - 1;
+                pageId = i;
+                break;
+            }
+        }
+        if (pageId == -1) {
+            pageId = LauncherSettings.Settings.call(mLauncher.getContentResolver(),
+                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
+                    .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+        }
+        for (int i = 0; i < mLauncher.getDeviceProfile().inv.numHotseatIcons; i++) {
+            View child = hotseatVG.getChildAt(i, 0);
+            if (child == null || child.getTag() == null) continue;
             ItemInfo tag = (ItemInfo) child.getTag();
             mLauncher.getModelWriter().moveItemInDatabase(tag,
-                    LauncherSettings.Favorites.CONTAINER_DESKTOP, toPage, tag.screenId, toRow);
+                    LauncherSettings.Favorites.CONTAINER_DESKTOP, pageId, i, toRow);
+            mNewItems.add(tag);
         }
-        return true;
+        return pageId;
     }
 
+
     void removeNotification() {
         mNotificationManager.cancel(ONBOARDING_NOTIFICATION_ID);
     }
 
     void finishOnboarding() {
-        mLauncher.getModel().rebindCallbacks();
+        mLauncher.getHotseat().removeAllViewsInLayout();
+        if (!mNewItems.isEmpty()) {
+            int lastPage = mNewItems.get(mNewItems.size() - 1).screenId;
+            ArrayList<ItemInfo> animated = new ArrayList<>();
+            ArrayList<ItemInfo> nonAnimated = new ArrayList<>();
+
+            for (ItemInfo info : mNewItems) {
+                if (info.screenId == lastPage) {
+                    animated.add(info);
+                } else {
+                    nonAnimated.add(info);
+                }
+            }
+            mLauncher.bindAppsAdded(mNewScreens, nonAnimated, animated);
+        }
+        mOnOnboardingComplete.run();
+        destroy();
         mLauncher.getSharedPrefs().edit().putBoolean(KEY_HOTSEAT_EDU_SEEN, true).apply();
-        removeNotification();
     }
 
     void setPredictedApps(List<WorkspaceItemInfo> predictedApps) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
index 538b7f3..bcce168 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
@@ -29,13 +29,13 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.android.launcher3.ArrowTipView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.WorkspaceLayoutManager;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.UserEventDispatcher;
@@ -54,23 +54,15 @@
     private static final int DEFAULT_CLOSE_DURATION = 200;
     protected static final int FINAL_SCRIM_BG_COLOR = 0x88000000;
 
-    // We don't migrate if user has more than SAME_PAGE_MAX_ROWS rows of item in their screen
-    private static final int SAME_PAGE_MAX_ROWS = 2;
 
-    private static final int MIGRATE_SAME_PAGE = 0;
-    private static final int MIGRATE_NEW_PAGE = 1;
-    private static final int MIGRATE_NO_MIGRATE = 2;
-
+    // we use this value to keep track of migration logs as we experiment with different migrations
+    private static final int MIGRATION_EXPERIMENT_IDENTIFIER = 1;
 
     private final Rect mInsets = new Rect();
     private View mHotseatWrapper;
     private CellLayout mSampleHotseat;
-    private TextView mEduHeading;
-    private TextView mEduContent;
     private Button mDismissBtn;
 
-    private int mMigrationMode = MIGRATE_SAME_PAGE;
-
     public void setHotseatEduController(HotseatEduController hotseatEduController) {
         mHotseatEduController = hotseatEduController;
     }
@@ -93,8 +85,6 @@
         super.onFinishInflate();
         mHotseatWrapper = findViewById(R.id.hotseat_wrapper);
         mSampleHotseat = findViewById(R.id.sample_prediction);
-        mEduHeading = findViewById(R.id.hotseat_edu_heading);
-        mEduContent = findViewById(R.id.hotseat_edu_content);
 
         DeviceProfile grid = mLauncher.getDeviceProfile();
         Rect padding = grid.getHotseatLayoutPadding();
@@ -109,25 +99,30 @@
         mDismissBtn = findViewById(R.id.no_thanks);
         mDismissBtn.setOnClickListener(this::onDismiss);
 
+        // update ui to reflect which migration method is going to be used
+        if (FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()) {
+            ((TextView) findViewById(R.id.hotseat_edu_content)).setText(
+                    R.string.hotseat_edu_message_migrate_alt);
+        }
     }
 
     private void onAccept(View v) {
-        if (mMigrationMode == MIGRATE_NO_MIGRATE || !mHotseatEduController.migrate()) {
-            onDismiss(v);
-            return;
-        }
+        mHotseatEduController.migrate();
         handleClose(true);
         mHotseatEduController.finishOnboarding();
-        logUserAction(true);
-        int toastStringRes = mMigrationMode == MIGRATE_SAME_PAGE
+        //TODO: pass actual page index here.
+        // Temporarily we're passing 1 for folder migration and 2 for page migration
+        logUserAction(true, FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get() ? 1 : 2);
+        int toastStringRes = !FeatureFlags.HOTSEAT_MIGRATE_TO_FOLDER.get()
                 ? R.string.hotseat_items_migrated : R.string.hotseat_items_migrated_alt;
         Toast.makeText(mLauncher, toastStringRes, Toast.LENGTH_LONG).show();
     }
 
     private void onDismiss(View v) {
-        Toast.makeText(getContext(), R.string.hotseat_no_migration, Toast.LENGTH_LONG).show();
+        int top = mLauncher.getHotseat().getTop();
+        new ArrowTipView(mLauncher).show(mLauncher.getString(R.string.hotseat_no_migration), top);
         mHotseatEduController.finishOnboarding();
-        logUserAction(false);
+        logUserAction(false, -1);
         handleClose(true);
     }
 
@@ -159,7 +154,7 @@
                 mLauncher.getDeviceProfile().hotseatBarSizePx + insets.bottom;
     }
 
-    private void logUserAction(boolean migrated) {
+    private void logUserAction(boolean migrated, int pageIndex) {
         LauncherLogProto.Action action = new LauncherLogProto.Action();
         LauncherLogProto.Target target = new LauncherLogProto.Target();
         action.type = LauncherLogProto.Action.Type.TOUCH;
@@ -168,8 +163,9 @@
         target.tipType = LauncherLogProto.TipType.HYBRID_HOTSEAT;
         target.controlType = migrated ? LauncherLogProto.ControlType.HYBRID_HOTSEAT_ACCEPTED
                 : HYBRID_HOTSEAT_CANCELED;
+        target.rank = MIGRATION_EXPERIMENT_IDENTIFIER;
         // encoding migration type on pageIndex
-        target.pageIndex = mMigrationMode;
+        target.pageIndex = pageIndex;
         LauncherLogProto.LauncherEvent event = newLauncherEvent(action, target);
         UserEventDispatcher.newInstance(getContext()).dispatchUserEvent(event, null);
     }
@@ -222,28 +218,6 @@
         }
     }
 
-    @Override
-    protected void attachToContainer() {
-        super.attachToContainer();
-        if (FeatureFlags.HOTSEAT_MIGRATE_NEW_PAGE.get()) {
-            mEduContent.setText(R.string.hotseat_edu_message_migrate_alt);
-            mMigrationMode = MIGRATE_NEW_PAGE;
-            return;
-        }
-        CellLayout page = mLauncher.getWorkspace().getScreenWithId(
-                WorkspaceLayoutManager.FIRST_SCREEN_ID);
-
-        int maxItemsOnPage = SAME_PAGE_MAX_ROWS * mLauncher.getDeviceProfile().inv.numColumns
-                + (FeatureFlags.QSB_ON_FIRST_SCREEN ? 1 : 0);
-        if (page.getShortcutsAndWidgets().getChildCount() > maxItemsOnPage
-                || !page.makeSpaceForHotseatMigration(false)) {
-            mMigrationMode = MIGRATE_NO_MIGRATE;
-            mEduContent.setText(R.string.hotseat_edu_message_no_migrate);
-            mEduHeading.setText(R.string.hotseat_edu_title_no_migrate);
-            mDismissBtn.setVisibility(GONE);
-        }
-    }
-
     /**
      * Opens User education dialog with a list of suggested apps
      */
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index 2cdcd20..d82e9f0 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -94,7 +94,6 @@
     private static final String BUNDLE_KEY_WORKSPACE = "workspace_apps";
 
     private static final String PREDICTION_CLIENT = "hotseat";
-
     private DropTarget.DragObject mDragObject;
     private int mHotSeatItemsCount;
     private int mPredictedSpotsCount = 0;
@@ -115,6 +114,7 @@
 
     private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>();
 
+
     private final View.OnLongClickListener mPredictionLongClickListener = v -> {
         if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
         if (mLauncher.getWorkspace().isSwitchingState()) return false;
@@ -276,12 +276,10 @@
                         .build());
         mAppPredictor.registerPredictionUpdates(mLauncher.getMainExecutor(),
                 this::setPredictedApps);
+        setPauseUIUpdate(false);
 
         if (!isReady()) {
-            if (mHotseatEduController != null) {
-                mHotseatEduController.destroy();
-            }
-            mHotseatEduController = new HotseatEduController(mLauncher);
+            mHotseatEduController = new HotseatEduController(mLauncher, this::createPredictor);
         }
         mAppPredictor.requestPredictionUpdate();
     }
@@ -327,7 +325,7 @@
             mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache));
         }
         predictionLog.append("]");
-        FileLog.d(TAG, predictionLog.toString());
+        if (false) FileLog.d(TAG, predictionLog.toString());
         updateDependencies();
         if (isReady()) {
             fillGapsWithPrediction();
@@ -488,7 +486,6 @@
         }
     }
 
-
     @Override
     public void onDragEnd() {
         if (mDragObject == null) {
@@ -564,7 +561,8 @@
     }
 
     @Override
-    public void reapplyItemInfo(ItemInfoWithIcon info) {}
+    public void reapplyItemInfo(ItemInfoWithIcon info) {
+    }
 
     @Override
     public void onDropCompleted(View target, DropTarget.DragObject d, boolean success) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index 5bac964..de3fce1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -19,7 +19,6 @@
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.quickstep.util.LayoutUtils;
@@ -107,7 +106,7 @@
     }
 
     @Override
-    public int getBackgroundBlurRadius(Context context) {
-        return context.getResources().getInteger(R.integer.app_background_blur_radius);
+    public float getDepth(Context context) {
+        return 1f;
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 6a34917..024872f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -207,8 +207,8 @@
     }
 
     @Override
-    public int getBackgroundBlurRadius(Context context) {
-        return context.getResources().getInteger(R.integer.overview_background_blur_radius);
+    public float getDepth(Context context) {
+        return 1f;
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
index aaf7619..ce7fa0d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -19,7 +19,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
-import static com.android.launcher3.uioverrides.BackgroundBlurController.BACKGROUND_BLUR;
+import static com.android.launcher3.uioverrides.DepthController.DEPTH;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
 
@@ -34,7 +34,7 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.uioverrides.BackgroundBlurController;
+import com.android.launcher3.uioverrides.DepthController;
 import com.android.quickstep.util.AppWindowAnimationHelper;
 import com.android.quickstep.util.AppWindowAnimationHelper.TransformParams;
 import com.android.quickstep.util.RemoteAnimationProvider;
@@ -105,10 +105,10 @@
             mRecentsView.setRunningTaskIconScaledDown(true);
         }
 
-        BackgroundBlurController blurController = mActivityInterface.getBackgroundBlurController();
-        if (blurController != null) {
+        DepthController depthController = mActivityInterface.getDepthController();
+        if (depthController != null) {
             // Update the surface to be the lowest closing app surface
-            blurController.setSurfaceToLauncher(mRecentsView);
+            depthController.setSurfaceToLauncher(mRecentsView);
         }
 
         AnimatorSet anim = new AnimatorSet();
@@ -124,7 +124,7 @@
         if (mActivity == null) {
             Log.e(TAG, "Animation created, before activity");
             anim.play(ValueAnimator.ofInt(0, 1).setDuration(RECENTS_LAUNCH_DURATION))
-                    .with(createBackgroundBlurAnimator(blurController));
+                    .with(createDepthAnimator(depthController));
             return anim;
         }
 
@@ -136,7 +136,7 @@
         if (runningTaskTarget == null) {
             Log.e(TAG, "No closing app");
             anim.play(ValueAnimator.ofInt(0, 1).setDuration(RECENTS_LAUNCH_DURATION))
-                    .with(createBackgroundBlurAnimator(blurController));
+                    .with(createDepthAnimator(depthController));
             return anim;
         }
 
@@ -184,7 +184,7 @@
             });
         }
         anim.play(valueAnimator)
-                .with(createBackgroundBlurAnimator(blurController));
+                .with(createDepthAnimator(depthController));
         return anim;
     }
 
@@ -197,14 +197,14 @@
         return RECENTS_LAUNCH_DURATION;
     }
 
-    private Animator createBackgroundBlurAnimator(BackgroundBlurController blurController) {
-        if (blurController == null) {
+    private Animator createDepthAnimator(DepthController depthController) {
+        if (depthController == null) {
             // Dummy animation
             return ValueAnimator.ofInt(0);
         }
-        return ObjectAnimator.ofInt(blurController, BACKGROUND_BLUR,
-                BACKGROUND_APP.getBackgroundBlurRadius(mActivity),
-                OVERVIEW.getBackgroundBlurRadius(mActivity))
+        return ObjectAnimator.ofFloat(depthController, DEPTH,
+                BACKGROUND_APP.getDepth(mActivity),
+                OVERVIEW.getDepth(mActivity))
                 .setDuration(RECENTS_LAUNCH_DURATION);
     }
 }
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 d402a75..55e6ba2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -56,8 +56,8 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.uioverrides.BackgroundBlurController;
-import com.android.launcher3.uioverrides.BackgroundBlurController.ClampedBlurProperty;
+import com.android.launcher3.uioverrides.DepthController;
+import com.android.launcher3.uioverrides.DepthController.ClampedDepthProperty;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.quickstep.SysUINavigationMode.Mode;
@@ -330,18 +330,17 @@
                     endState.getVerticalProgress(activity)));
         }
 
-        // Animate the blur
-        BackgroundBlurController blurController = getBackgroundBlurController();
-        int fromBlurRadius = fromState.getBackgroundBlurRadius(activity);
-        int toBlurRadius = endState.getBackgroundBlurRadius(activity);
-        Animator backgroundBlur = ObjectAnimator.ofInt(blurController,
-                new ClampedBlurProperty(toBlurRadius, fromBlurRadius),
-                fromBlurRadius, toBlurRadius);
-        anim.play(backgroundBlur);
+        // Animate the blur and wallpaper zoom
+        DepthController depthController = getDepthController();
+        float fromDepthRatio = fromState.getDepth(activity);
+        float toDepthRatio = endState.getDepth(activity);
+        Animator depthAnimator = ObjectAnimator.ofFloat(depthController,
+                new ClampedDepthProperty(fromDepthRatio, toDepthRatio),
+                fromDepthRatio, toDepthRatio);
+        anim.play(depthAnimator);
 
         playScaleDownAnim(anim, activity, fromState, endState);
 
-
         anim.setDuration(transitionLength * 2);
         anim.setInterpolator(LINEAR);
         AnimatorPlaybackController controller =
@@ -558,11 +557,11 @@
 
     @Nullable
     @Override
-    public BackgroundBlurController getBackgroundBlurController() {
+    public DepthController getDepthController() {
         BaseQuickstepLauncher launcher = getCreatedActivity();
         if (launcher == null) {
             return null;
         }
-        return launcher.getBackgroundBlurController();
+        return launcher.getDepthController();
     }
 }
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
index 3328abc..cafdb62 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
@@ -294,7 +294,7 @@
         }
 
         setupRecentsViewUi();
-        mActivityInterface.getBackgroundBlurController().setSurfaceToLauncher(mRecentsView);
+        mActivityInterface.getDepthController().setSurfaceToLauncher(mRecentsView);
 
         if (mDeviceState.getNavMode() == TWO_BUTTONS) {
             // If the device is in two button mode, swiping up will show overview with predictions
@@ -588,7 +588,7 @@
             // We will handle the sysui flags based on the centermost task view.
             if (mRecentsAnimationController != null) {
                 mRecentsAnimationController.setWindowThresholdCrossed(centermostTaskFlags != 0
-                        || useHomeScreenFlags);
+                        && useHomeScreenFlags);
             }
             int sysuiFlags = useHomeScreenFlags ? 0 : centermostTaskFlags;
             mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, sysuiFlags);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
index 3ab0f19..42d944f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
@@ -186,7 +186,7 @@
         AppWindowAnimationHelper helper = new AppWindowAnimationHelper(
             mFallbackRecentsView.getPagedViewOrientedState(), this);
         Animator recentsAnimator = getRecentsWindowAnimator(taskView, !activityClosing, appTargets,
-                wallpaperTargets, null /* backgroundBlurController */,
+                wallpaperTargets, null /* depthController */,
                 helper);
         target.play(recentsAnimator.setDuration(RECENTS_LAUNCH_DURATION));
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
index 38b86ce..7ec083e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskViewUtils.java
@@ -19,7 +19,7 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.uioverrides.BackgroundBlurController.BACKGROUND_BLUR;
+import static com.android.launcher3.uioverrides.DepthController.DEPTH;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
 
 import android.animation.Animator;
@@ -35,7 +35,7 @@
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.uioverrides.BackgroundBlurController;
+import com.android.launcher3.uioverrides.DepthController;
 import com.android.quickstep.util.AppWindowAnimationHelper;
 import com.android.quickstep.util.MultiValueUpdateListener;
 import com.android.quickstep.views.RecentsView;
@@ -123,7 +123,7 @@
     public static Animator getRecentsWindowAnimator(TaskView v, boolean skipViewChanges,
             RemoteAnimationTargetCompat[] appTargets,
             RemoteAnimationTargetCompat[] wallpaperTargets,
-            BackgroundBlurController backgroundBlurController,
+            DepthController depthController,
             final AppWindowAnimationHelper inOutHelper) {
         SyncRtSurfaceTransactionApplierCompat applier =
                 new SyncRtSurfaceTransactionApplierCompat(v);
@@ -215,9 +215,9 @@
             }
         });
 
-        if (backgroundBlurController != null) {
-            ObjectAnimator backgroundRadiusAnim = ObjectAnimator.ofInt(backgroundBlurController,
-                    BACKGROUND_BLUR, BACKGROUND_APP.getBackgroundBlurRadius(v.getContext()));
+        if (depthController != null) {
+            ObjectAnimator backgroundRadiusAnim = ObjectAnimator.ofFloat(depthController,
+                    DEPTH, BACKGROUND_APP.getDepth(v.getContext()));
             animatorSet.playTogether(appAnimator, backgroundRadiusAnim);
         } else {
             animatorSet.play(appAnimator);
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 25a3078..eb5c7f9 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -15,7 +15,9 @@
  */
 package com.android.quickstep;
 
+import static android.view.MotionEvent.ACTION_CANCEL;
 import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
 
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -435,7 +437,8 @@
                 TraceHelper.FLAG_ALLOW_BINDER_TRACKING);
         mDeviceState.setOrientationTransformIfNeeded(event);
 
-        if (event.getAction() == ACTION_DOWN) {
+        final int action = event.getAction();
+        if (action == ACTION_DOWN) {
             GestureState newGestureState = new GestureState(mOverviewComponentObserver,
                     ActiveGestureLog.INSTANCE.generateAndSetLogId());
             newGestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.0",
@@ -468,6 +471,13 @@
 
         ActiveGestureLog.INSTANCE.addLog("onMotionEvent", event.getActionMasked());
         mUncheckedConsumer.onMotionEvent(event);
+
+        if (action == ACTION_UP || action == ACTION_CANCEL) {
+            if (mConsumer != null && !mConsumer.isConsumerDetachedFromGesture()) {
+                onConsumerInactive(mConsumer);
+            }
+        }
+
         TraceHelper.INSTANCE.endFlagsOverride(traceToken);
     }
 
@@ -661,8 +671,8 @@
      */
     private void onConsumerInactive(InputConsumer caller) {
         if (mConsumer == caller) {
-            mConsumer = mResetGestureInputConsumer;
-            mUncheckedConsumer = mConsumer;
+            mConsumer = mUncheckedConsumer = mResetGestureInputConsumer;
+            mGestureState = new GestureState();
         }
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
index 2c5d631..7f5ec9b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/RecentsRootView.java
@@ -81,7 +81,8 @@
         if (!insets.equals(mInsets)) {
             super.setInsets(insets);
         }
-        setBackground(insets.top == 0 ? null
+        setBackground(insets.top == 0  || !mAllowSysuiScrims
+                ? null
                 : Themes.getAttrDrawable(getContext(), R.attr.workspaceStatusBarScrim));
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
index 05c206f..a87e7eb 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
@@ -25,6 +25,11 @@
     }
 
     @Override
+    public boolean isConsumerDetachedFromGesture() {
+        return mDelegate.isConsumerDetachedFromGesture();
+    }
+
+    @Override
     public boolean allowInterceptByParent() {
         return mDelegate.allowInterceptByParent() && 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 893868b..416d7a1 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
@@ -160,6 +160,11 @@
         return TYPE_OTHER_ACTIVITY;
     }
 
+    @Override
+    public boolean isConsumerDetachedFromGesture() {
+        return true;
+    }
+
     private void forceCancelGesture(MotionEvent ev) {
         int action = ev.getAction();
         ev.setAction(ACTION_CANCEL);
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 3ed7530..a027fea 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
@@ -46,9 +46,9 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
-import com.android.launcher3.states.RotationHelper;
-import com.android.launcher3.uioverrides.BackgroundBlurController;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.states.RotationHelper;
+import com.android.launcher3.uioverrides.DepthController;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.views.ScrimView;
@@ -409,7 +409,7 @@
     }
 
     @Override
-    protected BackgroundBlurController getBackgroundBlurController() {
-        return mActivity.getBackgroundBlurController();
+    protected DepthController getDepthController() {
+        return mActivity.getDepthController();
     }
 }
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 e15ac46..68c51a0 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
@@ -19,6 +19,7 @@
 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;
 import static com.android.launcher3.Utilities.squaredHypot;
@@ -29,7 +30,7 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.uioverrides.BackgroundBlurController.BACKGROUND_BLUR;
+import static com.android.launcher3.uioverrides.DepthController.DEPTH;
 import static com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
 import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CLEAR_ALL_BUTTON;
@@ -97,7 +98,7 @@
 import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.states.RotationHelper;
 import com.android.launcher3.touch.PagedOrientationHandler.CurveProperties;
-import com.android.launcher3.uioverrides.BackgroundBlurController;
+import com.android.launcher3.uioverrides.DepthController;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
@@ -717,6 +718,9 @@
         for (int i = 0; i < taskCount; i++) {
             getTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
         }
+        if (mActionsView != null) {
+            mActionsView.setVisibility(fullscreenProgress == 0 ? VISIBLE : INVISIBLE);
+        }
     }
 
     private void updateTaskStackListenerState() {
@@ -1162,7 +1166,9 @@
     }
 
     private void addDismissedTaskAnimations(View taskView, long duration, PendingAnimation anim) {
-        anim.setViewAlpha(taskView, 0, ACCEL_2);
+        // Use setFloat instead of setViewAlpha as we want to keep the view visible even when it's
+        // alpha is set to 0 so that it can be recycled in the view pool properly
+        anim.setFloat(taskView, VIEW_ALPHA, 0, ACCEL_2);
         FloatProperty<View> secondaryViewTranslate =
             mOrientationHandler.getSecondaryViewTranslate();
         int secondaryTaskDimension = mOrientationHandler.getSecondaryDimension(taskView);
@@ -1702,11 +1708,11 @@
         appWindowAnimationHelper.prepareAnimation(mActivity.getDeviceProfile(), true /* isOpening */);
         AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv, appWindowAnimationHelper);
 
-        BackgroundBlurController blurController = getBackgroundBlurController();
-        if (blurController != null) {
-            ObjectAnimator backgroundBlur = ObjectAnimator.ofInt(blurController, BACKGROUND_BLUR,
-                    BACKGROUND_APP.getBackgroundBlurRadius(mActivity));
-            anim.play(backgroundBlur);
+        DepthController depthController = getDepthController();
+        if (depthController != null) {
+            ObjectAnimator depthAnimator = ObjectAnimator.ofFloat(depthController, DEPTH,
+                    BACKGROUND_APP.getDepth(mActivity));
+            anim.play(depthAnimator);
         }
         anim.play(progressAnim);
         anim.setDuration(duration).setInterpolator(interpolator);
@@ -2009,7 +2015,7 @@
     }
 
     @Nullable
-    protected BackgroundBlurController getBackgroundBlurController() {
+    protected DepthController getDepthController() {
         return null;
     }
 
@@ -2029,7 +2035,6 @@
         void onEmptyMessageUpdated(boolean isEmpty);
     }
 
-
     private static class PinnedStackAnimationListener<T extends BaseActivity> extends
             IPinnedStackAnimationListener.Stub {
         private T mActivity;
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 c94b56c..56e3632 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
@@ -247,8 +247,17 @@
 
     /** Updates UI based on whether the task is modal. */
     public void updateUiForModalTask() {
+        boolean isOverlayModal = isTaskOverlayModal();
         if (getRecentsView() != null) {
-            getRecentsView().updateUiForModalTask(this, isTaskOverlayModal());
+            getRecentsView().updateUiForModalTask(this, isOverlayModal);
+        }
+        // Hide footers when overlay is modal.
+        if (isOverlayModal) {
+            for (FooterWrapper footer : mFooters) {
+                if (footer != null) {
+                    footer.animateHide();
+                }
+            }
         }
     }
 
@@ -780,6 +789,22 @@
             animator.setDuration(100);
             animator.start();
         }
+
+        void animateHide() {
+            ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 1.0f);
+            animator.addUpdateListener(anim -> {
+                mFooterVerticalOffset = anim.getAnimatedFraction();
+                updateFooterOffset();
+            });
+            animator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    removeView(mView);
+                }
+            });
+            animator.setDuration(100);
+            animator.start();
+        }
     }
 
     @Override
diff --git a/quickstep/res/layout/back_gesture_tutorial_fragment.xml b/quickstep/res/layout/back_gesture_tutorial_fragment.xml
index 294e46e..d8c25bd 100644
--- a/quickstep/res/layout/back_gesture_tutorial_fragment.xml
+++ b/quickstep/res/layout/back_gesture_tutorial_fragment.xml
@@ -16,9 +16,7 @@
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:layerType="software"
     android:background="@color/back_gesture_tutorial_background_color">
-    <!--The layout is rendered on the software layer to avoid b/136158117-->
 
     <ImageView
         android:id="@+id/back_gesture_tutorial_fragment_hand_coaching"
diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
new file mode 100644
index 0000000..3583676
--- /dev/null
+++ b/quickstep/res/values/colors.xml
@@ -0,0 +1,18 @@
+<?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.
+-->
+<resources>
+    <color name="back_arrow_color_dark">#99000000</color>
+</resources>
\ No newline at end of file
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 90d4245..b55b042 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -68,30 +68,25 @@
 
 
     <!-- Hotseat migration notification title -->
-    <string translatable="false" name="hotseat_edu_prompt_title">Get app suggestions based on your routines</string>
+    <string name="hotseat_edu_prompt_title">Easily access your most-used apps</string>
     <!-- Hotseat migration notification content -->
-    <string translatable="false" name="hotseat_edu_prompt_content">Tap to set up</string>
-
+    <string name="hotseat_edu_prompt_content">Pixel predicts apps you\’ll need next, right on your Home screen. Tap to set up.</string>
 
     <!-- Hotseat educational strings for users who don't qualify for migration -->
-    <string translatable="false" name="hotseat_edu_title_migrate">Suggested apps replace the bottom row of apps</string>
-    <string translatable="false" name="hotseat_edu_message_migrate">Your hotseat items will be moved up on the homescreen</string>
-    <string translatable="false" name="hotseat_edu_message_migrate_alt">Your hotseat items will be moved to the last page of your workspace</string>
+    <string name="hotseat_edu_title_migrate">Get app suggestions on the bottom row of your Home screen</string>
 
-
-    <!-- Hotseat educational strings for users who don't qualify -->
-    <string translatable="false" name="hotseat_edu_title_no_migrate">Suggested apps will be found at the bottom row of your home screen</string>
-    <string translatable="false" name="hotseat_edu_message_no_migrate">Drag one or many apps off the bottom row of home screen to see app suggestions</string>
+    <string name="hotseat_edu_message_migrate">Easily access your most-used apps right on the Home screen. Suggestions will change based on your routines. Apps on the bottom row will move up to your Home screen. </string>
+    <string name="hotseat_edu_message_migrate_alt">Easily access your most-used apps, right on the Home screen. Suggestions will change based on your routines. Apps on the bottom row will move to a new folder.</string>
 
     <!-- Toast message user sees after opting into fully predicted hybrid hotseat -->
-    <string translatable="false" name="hotseat_items_migrated">Bottom row of apps moved up.</string>
-    <string translatable="false" name="hotseat_items_migrated_alt">Bottom row of apps moved to last page.</string>
+    <string name="hotseat_items_migrated">Your hotseat items have been moved up to your homescreen</string>
+    <string name="hotseat_items_migrated_alt">Your hotseat items have been moved to a folder</string>
     <!-- Toast message user sees after opting into fully predicted hybrid hotseat -->
-    <string translatable="false" name="hotseat_no_migration">Bottom row won\'t be replaced. Manually drag apps for predictions.</string>
+    <string name="hotseat_no_migration">Drag apps off the bottom row to see app suggestions</string>
     <!-- Button text to opt in for fully predicted hotseat -->
-    <string translatable="false" name="hotseat_edu_accept">Got it</string>
+    <string name="hotseat_edu_accept">Get app suggestions</string>
     <!-- Button text to dismiss opt in for fully predicted hotseat -->
-    <string translatable="false" name="hotseat_edu_dismiss">No thanks</string>
+    <string name="hotseat_edu_dismiss">No thanks</string>
 
 
     <!-- Title shown during interactive part of Back gesture tutorial for right edge. [CHAR LIMIT=30] -->
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index a7d00c5..ec66f11 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -244,7 +244,7 @@
         return new StateHandler[] {
                 getAllAppsController(),
                 getWorkspace(),
-                getBackgroundBlurController(),
+                getDepthController(),
                 new RecentsViewStateController(this),
                 new BackButtonAlphaHandler(this)};
     }
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
index 1b39242..c93a4ba 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
@@ -33,7 +33,7 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.config.FeatureFlags.KEYGUARD_ANIMATION;
 import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_TRANSITIONS;
-import static com.android.launcher3.uioverrides.BackgroundBlurController.BACKGROUND_BLUR;
+import static com.android.launcher3.uioverrides.DepthController.DEPTH;
 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
 import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
 import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
@@ -69,11 +69,10 @@
 
 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
 import com.android.launcher3.allapps.AllAppsTransitionController;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.shortcuts.DeepShortcutView;
-import com.android.launcher3.uioverrides.BackgroundBlurController;
+import com.android.launcher3.uioverrides.DepthController;
 import com.android.launcher3.util.DynamicResource;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
@@ -387,18 +386,35 @@
             alpha.setInterpolator(LINEAR);
             launcherAnimator.play(alpha);
 
-            mDragLayer.setTranslationY(trans[0]);
-            ObjectAnimator transY = ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y, trans);
-            transY.setInterpolator(AGGRESSIVE_EASE);
-            transY.setDuration(CONTENT_TRANSLATION_DURATION);
-            launcherAnimator.play(transY);
+            Workspace workspace = mLauncher.getWorkspace();
+            View currentPage = ((CellLayout) workspace.getChildAt(workspace.getCurrentPage()))
+                    .getShortcutsAndWidgets();
+            View hotseat = mLauncher.getHotseat();
+            View qsb = mLauncher.findViewById(R.id.search_container_all_apps);
 
-            mDragLayer.getScrim().hideSysUiScrim(true);
+            currentPage.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+            hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+            qsb.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+
+            launcherAnimator.play(ObjectAnimator.ofFloat(currentPage, View.TRANSLATION_Y, trans));
+            launcherAnimator.play(ObjectAnimator.ofFloat(hotseat, View.TRANSLATION_Y, trans));
+            launcherAnimator.play(ObjectAnimator.ofFloat(qsb, View.TRANSLATION_Y, trans));
+
             // Pause page indicator animations as they lead to layer trashing.
             mLauncher.getWorkspace().getPageIndicator().pauseAnimations();
-            mDragLayer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
 
-            endListener = this::resetContentView;
+            endListener = () -> {
+                currentPage.setTranslationY(0);
+                hotseat.setTranslationY(0);
+                qsb.setTranslationY(0);
+
+                currentPage.setLayerType(View.LAYER_TYPE_NONE, null);
+                hotseat.setLayerType(View.LAYER_TYPE_NONE, null);
+                qsb.setLayerType(View.LAYER_TYPE_NONE, null);
+
+                mDragLayerAlpha.setValue(1f);
+                mLauncher.getWorkspace().getPageIndicator().skipAnimationsToEnd();
+            };
         }
         return new Pair<>(launcherAnimator, endListener);
     }
@@ -596,17 +612,17 @@
         // When launching an app from overview that doesn't map to a task, we still want to just
         // blur the wallpaper instead of the launcher surface as well
         boolean allowBlurringLauncher = mLauncher.getStateManager().getState() != OVERVIEW;
-        BackgroundBlurController blurController = mLauncher.getBackgroundBlurController();
-        ObjectAnimator backgroundRadiusAnim = ObjectAnimator.ofInt(blurController, BACKGROUND_BLUR,
-                BACKGROUND_APP.getBackgroundBlurRadius(mLauncher))
+        DepthController depthController = mLauncher.getDepthController();
+        ObjectAnimator backgroundRadiusAnim = ObjectAnimator.ofFloat(depthController, DEPTH,
+                BACKGROUND_APP.getDepth(mLauncher))
                 .setDuration(APP_LAUNCH_DURATION);
         if (allowBlurringLauncher) {
-            blurController.setSurfaceToApp(RemoteAnimationProvider.findLowestOpaqueLayerTarget(
+            depthController.setSurfaceToApp(RemoteAnimationProvider.findLowestOpaqueLayerTarget(
                     appTargets, MODE_OPENING));
             backgroundRadiusAnim.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
-                    blurController.setSurfaceToLauncher(mLauncher.getDragLayer());
+                    depthController.setSurfaceToLauncher(mLauncher.getDragLayer());
                 }
             });
         }
@@ -760,62 +776,6 @@
         return closingAnimator;
     }
 
-    /**
-     * Creates an animator that modifies Launcher as a result from 
-     * {@link #createWallpaperOpenRunner}.
-     */
-    private void createLauncherResumeAnimation(AnimatorSet anim) {
-        if (mLauncher.isInState(LauncherState.ALL_APPS)) {
-            Pair<AnimatorSet, Runnable> contentAnimator =
-                    getLauncherContentAnimator(false /* isAppOpening */,
-                            new float[] {-mContentTransY, 0});
-            contentAnimator.first.setStartDelay(LAUNCHER_RESUME_START_DELAY);
-            anim.play(contentAnimator.first);
-            anim.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    contentAnimator.second.run();
-                }
-            });
-        } else {
-            AnimatorSet workspaceAnimator = new AnimatorSet();
-
-            mDragLayer.setTranslationY(-mWorkspaceTransY);;
-            workspaceAnimator.play(ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y,
-                    -mWorkspaceTransY, 0));
-
-            mDragLayerAlpha.setValue(0);
-            workspaceAnimator.play(ObjectAnimator.ofFloat(
-                    mDragLayerAlpha, MultiValueAlpha.VALUE, 0, 1f));
-
-            workspaceAnimator.setStartDelay(LAUNCHER_RESUME_START_DELAY);
-            workspaceAnimator.setDuration(333);
-            workspaceAnimator.setInterpolator(Interpolators.DEACCEL_1_7);
-
-            mDragLayer.getScrim().hideSysUiScrim(true);
-
-            // Pause page indicator animations as they lead to layer trashing.
-            mLauncher.getWorkspace().getPageIndicator().pauseAnimations();
-            mDragLayer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
-
-            workspaceAnimator.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    resetContentView();
-                }
-            });
-            anim.play(workspaceAnimator);
-        }
-    }
-
-    private void resetContentView() {
-        mLauncher.getWorkspace().getPageIndicator().skipAnimationsToEnd();
-        mDragLayerAlpha.setValue(1f);
-        mDragLayer.setLayerType(View.LAYER_TYPE_NONE, null);
-        mDragLayer.setTranslationY(0f);
-        mDragLayer.getScrim().hideSysUiScrim(false);
-    }
-
     private boolean hasControlRemoteAppTransitionPermission() {
         return mLauncher.checkSelfPermission(CONTROL_REMOTE_APP_TRANSITION_PERMISSION)
                 == PackageManager.PERMISSION_GRANTED;
@@ -888,15 +848,25 @@
                     // Only register the content animation for cancellation when state changes
                     mLauncher.getStateManager().setCurrentAnimation(anim);
 
-                    if (mFromUnlock) {
+                    if (mLauncher.isInState(LauncherState.ALL_APPS)) {
+                        Pair<AnimatorSet, Runnable> contentAnimator =
+                                getLauncherContentAnimator(false /* isAppOpening */,
+                                        new float[] {-mContentTransY, 0});
+                        contentAnimator.first.setStartDelay(LAUNCHER_RESUME_START_DELAY);
+                        anim.play(contentAnimator.first);
+                        anim.addListener(new AnimatorListenerAdapter() {
+                            @Override
+                            public void onAnimationEnd(Animator animation) {
+                                contentAnimator.second.run();
+                            }
+                        });
+                    } else {
                         float velocityDpPerS = DynamicResource.provider(mLauncher)
                                 .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s);
                         float velocityPxPerS = TypedValue.applyDimension(COMPLEX_UNIT_DIP,
                                 velocityDpPerS, mLauncher.getResources().getDisplayMetrics());
                         anim.play(new StaggeredWorkspaceAnim(mLauncher, velocityPxPerS, false)
                                 .getAnimators());
-                    } else {
-                        createLauncherResumeAnimation(anim);
                     }
                 }
             }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BackgroundBlurController.java b/quickstep/src/com/android/launcher3/uioverrides/BackgroundBlurController.java
deleted file mode 100644
index 513310e..0000000
--- a/quickstep/src/com/android/launcher3/uioverrides/BackgroundBlurController.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.uioverrides;
-
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-
-import android.util.IntProperty;
-import android.view.View;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherStateManager;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.states.StateAnimationConfig;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SurfaceControlCompat;
-import com.android.systemui.shared.system.TransactionCompat;
-
-/**
- * Controls the blur, for the Launcher surface only.
- */
-public class BackgroundBlurController implements LauncherStateManager.StateHandler {
-
-    public static final IntProperty<BackgroundBlurController> BACKGROUND_BLUR =
-            new IntProperty<BackgroundBlurController>("backgroundBlur") {
-                @Override
-                public void setValue(BackgroundBlurController blurController, int blurRadius) {
-                    blurController.setBackgroundBlurRadius(blurRadius);
-                }
-
-                @Override
-                public Integer get(BackgroundBlurController blurController) {
-                    return blurController.mBackgroundBlurRadius;
-                }
-            };
-
-    /**
-     * A property that updates the background blur within a given range of values (ie. even if the
-     * animator goes beyond 0..1, the interpolated value will still be bounded).
-     */
-    public static class ClampedBlurProperty extends IntProperty<BackgroundBlurController> {
-        private final int mMinValue;
-        private final int mMaxValue;
-
-        public ClampedBlurProperty(int minValue, int maxValue) {
-            super(("backgroundBlurClamped"));
-            mMinValue = minValue;
-            mMaxValue = maxValue;
-        }
-
-        @Override
-        public void setValue(BackgroundBlurController blurController, int blurRadius) {
-            blurController.setBackgroundBlurRadius(Utilities.boundToRange(blurRadius,
-                    mMinValue, mMaxValue));
-        }
-
-        @Override
-        public Integer get(BackgroundBlurController blurController) {
-            return blurController.mBackgroundBlurRadius;
-        }
-    }
-
-    private final Launcher mLauncher;
-    private SurfaceControlCompat mSurface;
-    private int mBackgroundBlurRadius;
-
-    public BackgroundBlurController(Launcher l) {
-        mLauncher = l;
-    }
-
-    /**
-     * @return the background blur adjustment for folders
-     */
-    public int getFolderBackgroundBlurAdjustment() {
-        return mLauncher.getResources().getInteger(
-                R.integer.folder_background_blur_radius_adjustment);
-    }
-
-    /**
-     * Sets the specified app target surface to apply the blur to.
-     */
-    public void setSurfaceToApp(RemoteAnimationTargetCompat target) {
-        if (target != null) {
-            setSurface(target.leash);
-        }
-    }
-
-    /**
-     * Sets the surface to apply the blur to as the launcher surface.
-     */
-    public void setSurfaceToLauncher(View v) {
-        setSurface(v != null ? new SurfaceControlCompat(v) : null);
-    }
-
-    private void setSurface(SurfaceControlCompat surface) {
-        if (mSurface != surface) {
-            mSurface = surface;
-            if (surface != null) {
-                setBackgroundBlurRadius(mBackgroundBlurRadius);
-            } else {
-                // If there is no surface, then reset the blur radius
-                setBackgroundBlurRadius(0);
-            }
-        }
-    }
-
-    @Override
-    public void setState(LauncherState toState) {
-        if (mSurface == null) {
-            return;
-        }
-
-        int toBackgroundBlurRadius = toState.getBackgroundBlurRadius(mLauncher);
-        if (mBackgroundBlurRadius != toBackgroundBlurRadius) {
-            setBackgroundBlurRadius(toBackgroundBlurRadius);
-        }
-    }
-
-    @Override
-    public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
-            PendingAnimation animation) {
-        if (mSurface == null || config.onlyPlayAtomicComponent()) {
-            return;
-        }
-
-        int toBackgroundBlurRadius = toState.getBackgroundBlurRadius(mLauncher);
-        if (mBackgroundBlurRadius != toBackgroundBlurRadius) {
-            animation.setInt(this, BACKGROUND_BLUR, toBackgroundBlurRadius, LINEAR);
-        }
-    }
-
-    private void setBackgroundBlurRadius(int blurRadius) {
-        // TODO: Do nothing if the shadows are not enabled
-        // Always update the background blur as it will be reapplied when a surface is next
-        // available
-        mBackgroundBlurRadius = blurRadius;
-        if (mSurface == null || !mSurface.isValid()) {
-            return;
-        }
-        new TransactionCompat()
-                .setBackgroundBlurRadius(mSurface, blurRadius)
-                .apply();
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DepthController.java b/quickstep/src/com/android/launcher3/uioverrides/DepthController.java
new file mode 100644
index 0000000..8995a7e
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/DepthController.java
@@ -0,0 +1,172 @@
+/*
+ * 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.uioverrides;
+
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+
+import android.os.IBinder;
+import android.util.FloatProperty;
+import android.view.View;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SurfaceControlCompat;
+import com.android.systemui.shared.system.TransactionCompat;
+import com.android.systemui.shared.system.WallpaperManagerCompat;
+
+/**
+ * Controls blur and wallpaper zoom, for the Launcher surface only.
+ */
+public class DepthController implements LauncherStateManager.StateHandler {
+
+    public static final FloatProperty<DepthController> DEPTH =
+            new FloatProperty<DepthController>("depth") {
+                @Override
+                public void setValue(DepthController depthController, float depth) {
+                    depthController.setDepth(depth);
+                }
+
+                @Override
+                public Float get(DepthController depthController) {
+                    return depthController.mDepth;
+                }
+            };
+
+    /**
+     * A property that updates the background blur within a given range of values (ie. even if the
+     * animator goes beyond 0..1, the interpolated value will still be bounded).
+     */
+    public static class ClampedDepthProperty extends FloatProperty<DepthController> {
+        private final float mMinValue;
+        private final float mMaxValue;
+
+        public ClampedDepthProperty(float minValue, float maxValue) {
+            super("depthClamped");
+            mMinValue = minValue;
+            mMaxValue = maxValue;
+        }
+
+        @Override
+        public void setValue(DepthController depthController, float depth) {
+            depthController.setDepth(Utilities.boundToRange(depth, mMinValue, mMaxValue));
+        }
+
+        @Override
+        public Float get(DepthController depthController) {
+            return depthController.mDepth;
+        }
+    }
+
+    private final Launcher mLauncher;
+    /**
+     * Blur radius when completely zoomed out, in pixels.
+     */
+    private int mMaxBlurRadius;
+    private WallpaperManagerCompat mWallpaperManager;
+    private SurfaceControlCompat mSurface;
+    /**
+     * Ratio from 0 to 1, where 0 is fully zoomed out, and 1 is zoomed in.
+     * @see android.service.wallpaper.WallpaperService.Engine#onZoomChanged(float)
+     */
+    private float mDepth;
+
+    public DepthController(Launcher l) {
+        mLauncher = l;
+    }
+
+    private void ensureDependencies() {
+        if (mWallpaperManager != null) {
+            return;
+        }
+        mMaxBlurRadius = mLauncher.getResources().getInteger(R.integer.max_depth_blur_radius);
+        mWallpaperManager = new WallpaperManagerCompat(mLauncher);
+    }
+
+    /**
+     * Sets the specified app target surface to apply the blur to.
+     */
+    public void setSurfaceToApp(RemoteAnimationTargetCompat target) {
+        if (target != null) {
+            setSurface(target.leash);
+        }
+    }
+
+    /**
+     * Sets the surface to apply the blur to as the launcher surface.
+     */
+    public void setSurfaceToLauncher(View v) {
+        setSurface(v != null ? new SurfaceControlCompat(v) : null);
+    }
+
+    private void setSurface(SurfaceControlCompat surface) {
+        if (mSurface != surface) {
+            mSurface = surface;
+            if (surface != null) {
+                setDepth(mDepth);
+            } else {
+                // If there is no surface, then reset the ratio
+                setDepth(0f);
+            }
+        }
+    }
+
+    @Override
+    public void setState(LauncherState toState) {
+        if (mSurface == null) {
+            return;
+        }
+
+        float toDepth = toState.getDepth(mLauncher);
+        if (Float.compare(mDepth, toDepth) != 0) {
+            setDepth(toDepth);
+        }
+    }
+
+    @Override
+    public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
+            PendingAnimation animation) {
+        if (mSurface == null || config.onlyPlayAtomicComponent()) {
+            return;
+        }
+
+        float toDepth = toState.getDepth(mLauncher);
+        if (Float.compare(mDepth, toDepth) != 0) {
+            animation.setFloat(this, DEPTH, toDepth, LINEAR);
+        }
+    }
+
+    private void setDepth(float depth) {
+        mDepth = depth;
+        if (mSurface == null || !mSurface.isValid()) {
+            return;
+        }
+        ensureDependencies();
+        IBinder windowToken = mLauncher.getRootView().getWindowToken();
+        if (windowToken != null) {
+            mWallpaperManager.setWallpaperZoomOut(windowToken, mDepth);
+        }
+        new TransactionCompat()
+                .setBackgroundBlurRadius(mSurface, (int) (mDepth * mMaxBlurRadius))
+                .apply();
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PreviewSurfaceRenderer.java b/quickstep/src/com/android/launcher3/uioverrides/PreviewSurfaceRenderer.java
index 548223a..c7cce0b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PreviewSurfaceRenderer.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PreviewSurfaceRenderer.java
@@ -32,8 +32,11 @@
 
     /** Handle a received surface view request. */
     public static void render(Context context, Bundle bundle) {
-        final String gridName = bundle.getString("name");
+        String gridName = bundle.getString("name");
         bundle.remove("name");
+        if (gridName == null) {
+            gridName = InvariantDeviceProfile.getCurrentGridName(context);
+        }
         final InvariantDeviceProfile idp = new InvariantDeviceProfile(context, gridName);
 
         MAIN_EXECUTOR.execute(() -> {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index 971d917..93e02a1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -22,7 +22,6 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.quickstep.SysUINavigationMode;
@@ -88,8 +87,8 @@
     }
 
     @Override
-    public int getBackgroundBlurRadius(Context context) {
-        return context.getResources().getInteger(R.integer.allapps_background_blur_radius);
+    public float getDepth(Context context) {
+        return 1f;
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index be0bdd8..2a569f5 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -33,7 +33,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.touch.PagedOrientationHandler;
-import com.android.launcher3.uioverrides.BackgroundBlurController;
+import com.android.launcher3.uioverrides.DepthController;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.ShelfPeekAnim;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -81,7 +81,8 @@
     @Nullable
     T getCreatedActivity();
 
-    default @Nullable BackgroundBlurController getBackgroundBlurController() {
+    @Nullable
+    default DepthController getDepthController() {
         return null;
     }
 
diff --git a/quickstep/src/com/android/quickstep/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
index 3e84e7d..8efaeb9 100644
--- a/quickstep/src/com/android/quickstep/InputConsumer.java
+++ b/quickstep/src/com/android/quickstep/InputConsumer.java
@@ -60,6 +60,16 @@
     }
 
     /**
+     * Returns true if the lifecycle of this input consumer is detached from the normal gesture
+     * down/up flow. If so, it is the responsibility of the input consumer to call back to
+     * {@link TouchInteractionService#onConsumerInactive(InputConsumer)} after the consumer is
+     * finished.
+     */
+    default boolean isConsumerDetachedFromGesture() {
+        return false;
+    }
+
+    /**
      * Called by the event queue when the consumer is about to be switched to a new consumer.
      * Consumers should update the state accordingly here before the state is passed to the new
      * consumer.
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
index 3fe91a3..5c2e992 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -25,6 +25,7 @@
 import com.android.launcher3.R;
 import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
 import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
 
 import java.util.Optional;
 
@@ -79,21 +80,33 @@
         mHandCoachingAnimation.stop();
     }
 
-    void onGestureDetected() {
-        hideHandCoachingAnimation();
-
-        if (mTutorialStep == TutorialStep.CONFIRM) {
+    void onGestureAttempted(BackGestureResult result) {
+        if (mTutorialStep == TutorialStep.CONFIRM
+                && (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
+                    || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT)) {
             mFragment.closeTutorial();
             return;
         }
 
-        if (mTutorialTypeInfo.get().getTutorialType() == TutorialType.RIGHT_EDGE_BACK_NAVIGATION) {
-            mFragment.changeController(TutorialStep.ENGAGED,
-                    TutorialType.LEFT_EDGE_BACK_NAVIGATION);
+        if (!mTutorialTypeInfo.isPresent()) {
             return;
         }
 
-        mFragment.changeController(TutorialStep.CONFIRM);
+        switch (mTutorialTypeInfo.get().getTutorialType()) {
+            case RIGHT_EDGE_BACK_NAVIGATION:
+                if (result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
+                    hideHandCoachingAnimation();
+                    mFragment.changeController(
+                            TutorialStep.ENGAGED, TutorialType.LEFT_EDGE_BACK_NAVIGATION);
+                }
+                break;
+            case LEFT_EDGE_BACK_NAVIGATION:
+                if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT) {
+                    hideHandCoachingAnimation();
+                    mFragment.changeController(TutorialStep.CONFIRM);
+                }
+                break;
+        }
     }
 
     abstract Optional<Integer> getTitleStringId();
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
index 54408ce..593b695 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
@@ -17,21 +17,26 @@
 
 import android.content.ActivityNotFoundException;
 import android.content.Intent;
+import android.graphics.Insets;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.WindowInsets;
 
+import androidx.annotation.NonNull;
 import androidx.fragment.app.Fragment;
 
 import com.android.launcher3.R;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
 
 import java.net.URISyntaxException;
 import java.util.Optional;
 
 /** Shows the Back gesture interactive tutorial. */
-public class BackGestureTutorialFragment extends Fragment {
+public class BackGestureTutorialFragment extends Fragment implements BackGestureAttemptCallback {
 
     private static final String LOG_TAG = "TutorialFragment";
     private static final String KEY_TUTORIAL_STEP = "tutorialStep";
@@ -47,6 +52,7 @@
     private Optional<BackGestureTutorialController> mTutorialController = Optional.empty();
     private View mRootView;
     private BackGestureTutorialHandAnimation mHandCoachingAnimation;
+    private EdgeBackGestureHandler mEdgeBackGestureHandler;
 
     public static BackGestureTutorialFragment newInstance(
             TutorialStep tutorialStep, TutorialType tutorialType) {
@@ -64,17 +70,25 @@
         Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
         mTutorialStep = (TutorialStep) args.getSerializable(KEY_TUTORIAL_STEP);
         mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
+        mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext());
+        mEdgeBackGestureHandler.registerBackGestureAttemptCallback(this);
     }
 
     @Override
     public View onCreateView(
-            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+            @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         super.onCreateView(inflater, container, savedInstanceState);
 
         mRootView = inflater.inflate(R.layout.back_gesture_tutorial_fragment,
                 container, /* attachToRoot= */ false);
         mRootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button)
                 .setOnClickListener(this::onCloseButtonClicked);
+        mRootView.setOnApplyWindowInsetsListener((view, insets) -> {
+            Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());
+            mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right);
+            return insets;
+        });
+        mRootView.setOnTouchListener(mEdgeBackGestureHandler);
         mHandCoachingAnimation = new BackGestureTutorialHandAnimation(getContext(), mRootView);
 
         return mRootView;
@@ -92,6 +106,14 @@
         mHandCoachingAnimation.stop();
     }
 
+    void onAttachedToWindow() {
+        mEdgeBackGestureHandler.setIsEnabled(true);
+    }
+
+    void onDetachedFromWindow() {
+        mEdgeBackGestureHandler.setIsEnabled(false);
+    }
+
     @Override
     public void onSaveInstanceState(Bundle savedInstanceState) {
         savedInstanceState.putSerializable(KEY_TUTORIAL_STEP, mTutorialStep);
@@ -125,10 +147,9 @@
         this.mTutorialType = tutorialType;
     }
 
-    void onBackPressed() {
-        if (mTutorialController.isPresent()) {
-            mTutorialController.get().onGestureDetected();
-        }
+    @Override
+    public void onBackGestureAttempted(BackGestureResult result) {
+        mTutorialController.ifPresent(controller -> controller.onGestureAttempted(result));
     }
 
     void closeTutorial() {
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
new file mode 100644
index 0000000..04cd2f4
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
@@ -0,0 +1,274 @@
+/*
+ * 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.interaction;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemProperties;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+
+import com.android.launcher3.ResourceUtils;
+
+/**
+ * Utility class to handle edge swipes for back gestures.
+ *
+ * Forked from platform/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java.
+ */
+public class EdgeBackGestureHandler implements DisplayListener, OnTouchListener {
+
+    private static final String TAG = "EdgeBackGestureHandler";
+    private static final int MAX_LONG_PRESS_TIMEOUT = SystemProperties.getInt(
+            "gestures.back_timeout", 250);
+
+    private final Context mContext;
+
+    private final Point mDisplaySize = new Point();
+    private final int mDisplayId;
+
+    // The edge width where touch down is allowed
+    private int mEdgeWidth;
+    // The bottom gesture area height
+    private int mBottomGestureHeight;
+    // The slop to distinguish between horizontal and vertical motion
+    private final float mTouchSlop;
+    // Duration after which we consider the event as longpress.
+    private final int mLongPressTimeout;
+
+    private final PointF mDownPoint = new PointF();
+    private boolean mThresholdCrossed = false;
+    private boolean mAllowGesture = false;
+    private boolean mIsEnabled;
+    private int mLeftInset;
+    private int mRightInset;
+
+    private EdgeBackGesturePanel mEdgeBackPanel;
+    private BackGestureAttemptCallback mGestureCallback;
+
+    private final EdgeBackGesturePanel.BackCallback mBackCallback =
+            new EdgeBackGesturePanel.BackCallback() {
+                @Override
+                public void triggerBack() {
+                    if (mGestureCallback != null) {
+                        mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel()
+                                ? BackGestureResult.BACK_COMPLETED_FROM_LEFT
+                                : BackGestureResult.BACK_COMPLETED_FROM_RIGHT);
+                    }
+                }
+
+                @Override
+                public void cancelBack() {
+                    if (mGestureCallback != null) {
+                        mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel()
+                                ? BackGestureResult.BACK_CANCELLED_FROM_LEFT
+                                : BackGestureResult.BACK_CANCELLED_FROM_RIGHT);
+                    }
+                }
+            };
+
+    EdgeBackGestureHandler(Context context) {
+        final Resources res = context.getResources();
+        mContext = context;
+        mDisplayId = context.getDisplay() == null
+                ? Display.DEFAULT_DISPLAY : context.getDisplay().getDisplayId();
+
+        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+        mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT,
+                ViewConfiguration.getLongPressTimeout());
+
+        mBottomGestureHeight =
+            ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, res);
+        mEdgeWidth = ResourceUtils.getNavbarSize("config_backGestureInset", res);
+    }
+
+    void setIsEnabled(boolean isEnabled) {
+        if (isEnabled == mIsEnabled) {
+            return;
+        }
+        mIsEnabled = isEnabled;
+
+        if (mEdgeBackPanel != null) {
+            mEdgeBackPanel.onDestroy();
+            mEdgeBackPanel = null;
+        }
+
+        if (!mIsEnabled) {
+            mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
+        } else {
+            updateDisplaySize();
+            mContext.getSystemService(DisplayManager.class).registerDisplayListener(this,
+                    new Handler(Looper.getMainLooper()));
+
+            // Add a nav bar panel window.
+            mEdgeBackPanel = new EdgeBackGesturePanel(mContext);
+            mEdgeBackPanel.setBackCallback(mBackCallback);
+            mEdgeBackPanel.setLayoutParams(createLayoutParams());
+            updateDisplaySize();
+        }
+    }
+
+    void registerBackGestureAttemptCallback(BackGestureAttemptCallback callback) {
+        mGestureCallback = callback;
+    }
+
+    private WindowManager.LayoutParams createLayoutParams() {
+        Resources resources = mContext.getResources();
+        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
+                ResourceUtils.getNavbarSize("navigation_edge_panel_width", resources),
+                ResourceUtils.getNavbarSize("navigation_edge_panel_height", resources),
+                LayoutParams.TYPE_APPLICATION_PANEL,
+                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+                        | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
+                PixelFormat.TRANSLUCENT);
+        layoutParams.setTitle(TAG + mDisplayId);
+        layoutParams.windowAnimations = 0;
+        layoutParams.setFitInsetsTypes(0 /* types */);
+        return layoutParams;
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent motionEvent) {
+        if (mIsEnabled) {
+            onMotionEvent(motionEvent);
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isWithinTouchRegion(int x, int y) {
+        // Disallow if too far from the edge
+        if (x > mEdgeWidth + mLeftInset && x < (mDisplaySize.x - mEdgeWidth - mRightInset)) {
+            return false;
+        }
+
+        // Disallow if we are in the bottom gesture area
+        if (y >= (mDisplaySize.y - mBottomGestureHeight)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private void cancelGesture(MotionEvent ev) {
+        // Send action cancel to reset all the touch events
+        mAllowGesture = false;
+        MotionEvent cancelEv = MotionEvent.obtain(ev);
+        cancelEv.setAction(MotionEvent.ACTION_CANCEL);
+        mEdgeBackPanel.onMotionEvent(cancelEv);
+        cancelEv.recycle();
+    }
+
+    private void onMotionEvent(MotionEvent ev) {
+        int action = ev.getActionMasked();
+        if (action == MotionEvent.ACTION_DOWN) {
+            boolean isOnLeftEdge = ev.getX() <= mEdgeWidth + mLeftInset;
+            mAllowGesture = isWithinTouchRegion((int) ev.getX(), (int) ev.getY());
+            if (mAllowGesture) {
+                mEdgeBackPanel.setIsLeftPanel(isOnLeftEdge);
+                mEdgeBackPanel.onMotionEvent(ev);
+
+                mDownPoint.set(ev.getX(), ev.getY());
+                mThresholdCrossed = false;
+            }
+        } else if (mAllowGesture) {
+            if (!mThresholdCrossed) {
+                if (action == MotionEvent.ACTION_POINTER_DOWN) {
+                    // We do not support multi touch for back gesture
+                    cancelGesture(ev);
+                    return;
+                } else if (action == MotionEvent.ACTION_MOVE) {
+                    if ((ev.getEventTime() - ev.getDownTime()) > mLongPressTimeout) {
+                        cancelGesture(ev);
+                        return;
+                    }
+                    float dx = Math.abs(ev.getX() - mDownPoint.x);
+                    float dy = Math.abs(ev.getY() - mDownPoint.y);
+                    if (dy > dx && dy > mTouchSlop) {
+                        cancelGesture(ev);
+                        return;
+
+                    } else if (dx > dy && dx > mTouchSlop) {
+                        mThresholdCrossed = true;
+                    }
+                }
+
+            }
+
+            // forward touch
+            mEdgeBackPanel.onMotionEvent(ev);
+        }
+
+        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+            if (!mAllowGesture && mGestureCallback != null) {
+                mGestureCallback.onBackGestureAttempted(BackGestureResult.BACK_NOT_STARTED);
+            }
+        }
+    }
+
+    @Override
+    public void onDisplayAdded(int displayId) { }
+
+    @Override
+    public void onDisplayRemoved(int displayId) { }
+
+    @Override
+    public void onDisplayChanged(int displayId) {
+        if (displayId == mDisplayId) {
+            updateDisplaySize();
+        }
+    }
+
+    private void updateDisplaySize() {
+        mContext.getDisplay().getRealSize(mDisplaySize);
+        if (mEdgeBackPanel != null) {
+            mEdgeBackPanel.setDisplaySize(mDisplaySize);
+        }
+    }
+
+    void setInsets(int leftInset, int rightInset) {
+        mLeftInset = leftInset;
+        mRightInset = rightInset;
+    }
+
+    enum BackGestureResult {
+        UNKNOWN,
+        BACK_COMPLETED_FROM_LEFT,
+        BACK_COMPLETED_FROM_RIGHT,
+        BACK_CANCELLED_FROM_LEFT,
+        BACK_CANCELLED_FROM_RIGHT,
+        BACK_NOT_STARTED,
+    }
+
+    /** Callback to let the UI react to attempted back gestures. */
+    interface BackGestureAttemptCallback {
+        /** Called whenever any touch is completed. */
+        void onBackGestureAttempted(BackGestureResult result);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java
new file mode 100644
index 0000000..34eeafc
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java
@@ -0,0 +1,701 @@
+/*
+ * 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.interaction;
+
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.os.SystemClock;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+import androidx.core.math.MathUtils;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.launcher3.R;
+import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.util.VibratorWrapper;
+
+/** Forked from platform/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java. */
+public class EdgeBackGesturePanel extends View {
+
+    private static final String LOG_TAG = "EdgeBackGesturePanel";
+
+    private static final long DISAPPEAR_FADE_ANIMATION_DURATION_MS = 80;
+    private static final long DISAPPEAR_ARROW_ANIMATION_DURATION_MS = 100;
+
+    /**
+     * The time required since the first vibration effect to automatically trigger a click
+     */
+    private static final int GESTURE_DURATION_FOR_CLICK_MS = 400;
+
+    /**
+     * The basic translation in dp where the arrow resides
+     */
+    private static final int BASE_TRANSLATION_DP = 32;
+
+    /**
+     * The length of the arrow leg measured from the center to the end
+     */
+    private static final int ARROW_LENGTH_DP = 18;
+
+    /**
+     * The angle measured from the xAxis, where the leg is when the arrow rests
+     */
+    private static final int ARROW_ANGLE_WHEN_EXTENDED_DEGREES = 56;
+
+    /**
+     * The angle that is added per 1000 px speed to the angle of the leg
+     */
+    private static final int ARROW_ANGLE_ADDED_PER_1000_SPEED = 4;
+
+    /**
+     * The maximum angle offset allowed due to speed
+     */
+    private static final int ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES = 4;
+
+    /**
+     * The thickness of the arrow. Adjusted to match the home handle (approximately)
+     */
+    private static final float ARROW_THICKNESS_DP = 2.5f;
+
+    /**
+     * The amount of rubber banding we do for the vertical translation
+     */
+    private static final int RUBBER_BAND_AMOUNT = 15;
+
+    /**
+     * The interpolator used to rubberband
+     */
+    private static final Interpolator RUBBER_BAND_INTERPOLATOR =
+            new PathInterpolator(1.0f / 5.0f, 1.0f, 1.0f, 1.0f);
+
+    /**
+     * The amount of rubber banding we do for the translation before base translation
+     */
+    private static final int RUBBER_BAND_AMOUNT_APPEAR = 4;
+
+    /**
+     * The interpolator used to rubberband the appearing of the arrow.
+     */
+    private static final Interpolator RUBBER_BAND_INTERPOLATOR_APPEAR =
+            new PathInterpolator(1.0f / RUBBER_BAND_AMOUNT_APPEAR, 1.0f, 1.0f, 1.0f);
+
+    private final WindowManager mWindowManager;
+    private BackCallback mBackCallback;
+
+    /**
+     * The paint the arrow is drawn with
+     */
+    private final Paint mPaint = new Paint();
+
+    private final float mDensity;
+    private final float mBaseTranslation;
+    private final float mArrowLength;
+    private final float mArrowThickness;
+
+    /**
+     * The minimum delta needed in movement for the arrow to change direction / stop triggering back
+     */
+    private final float mMinDeltaForSwitch;
+    // The closest to y = 0 that the arrow will be displayed.
+    private int mMinArrowPosition;
+    // The amount the arrow is shifted to avoid the finger.
+    private int mFingerOffset;
+
+    private final float mSwipeThreshold;
+    private final Path mArrowPath = new Path();
+    private final Point mDisplaySize = new Point();
+
+    private final SpringAnimation mAngleAnimation;
+    private final SpringAnimation mTranslationAnimation;
+    private final SpringAnimation mVerticalTranslationAnimation;
+    private final SpringForce mAngleAppearForce;
+    private final SpringForce mAngleDisappearForce;
+    private final ValueAnimator mArrowDisappearAnimation;
+    private final SpringForce mRegularTranslationSpring;
+    private final SpringForce mTriggerBackSpring;
+
+    private VelocityTracker mVelocityTracker;
+    private int mArrowPaddingEnd;
+    private WindowManager.LayoutParams mLayoutParams;
+
+    /**
+     * True if the panel is currently on the left of the screen
+     */
+    private boolean mIsLeftPanel;
+
+    private float mStartX;
+    private float mStartY;
+    private float mCurrentAngle;
+    /**
+     * The current translation of the arrow
+     */
+    private float mCurrentTranslation;
+    /**
+     * Where the arrow will be in the resting position.
+     */
+    private float mDesiredTranslation;
+
+    private boolean mDragSlopPassed;
+    private boolean mArrowsPointLeft;
+    private float mMaxTranslation;
+    private boolean mTriggerBack;
+    private float mPreviousTouchTranslation;
+    private float mTotalTouchDelta;
+    private float mVerticalTranslation;
+    private float mDesiredVerticalTranslation;
+    private float mDesiredAngle;
+    private float mAngleOffset;
+    private float mDisappearAmount;
+    private long mVibrationTime;
+    private int mScreenSize;
+
+    private final DynamicAnimation.OnAnimationEndListener mSetGoneEndListener =
+            new DynamicAnimation.OnAnimationEndListener() {
+                @Override
+                public void onAnimationEnd(
+                        DynamicAnimation animation, boolean canceled, float value, float velocity) {
+                    animation.removeEndListener(this);
+                    if (!canceled) {
+                        setVisibility(GONE);
+                    }
+                }
+            };
+
+    private static final FloatPropertyCompat<EdgeBackGesturePanel> CURRENT_ANGLE =
+            new FloatPropertyCompat<EdgeBackGesturePanel>("currentAngle") {
+                @Override
+                public void setValue(EdgeBackGesturePanel object, float value) {
+                    object.setCurrentAngle(value);
+                }
+
+                @Override
+                public float getValue(EdgeBackGesturePanel object) {
+                    return object.getCurrentAngle();
+                }
+            };
+
+    private static final FloatPropertyCompat<EdgeBackGesturePanel> CURRENT_TRANSLATION =
+            new FloatPropertyCompat<EdgeBackGesturePanel>("currentTranslation") {
+                @Override
+                public void setValue(EdgeBackGesturePanel object, float value) {
+                    object.setCurrentTranslation(value);
+                }
+
+                @Override
+                public float getValue(EdgeBackGesturePanel object) {
+                    return object.getCurrentTranslation();
+                }
+            };
+
+    private static final FloatPropertyCompat<EdgeBackGesturePanel> CURRENT_VERTICAL_TRANSLATION =
+            new FloatPropertyCompat<EdgeBackGesturePanel>("verticalTranslation") {
+
+                @Override
+                public void setValue(EdgeBackGesturePanel object, float value) {
+                    object.setVerticalTranslation(value);
+                }
+
+                @Override
+                public float getValue(EdgeBackGesturePanel object) {
+                    return object.getVerticalTranslation();
+                }
+            };
+
+    public EdgeBackGesturePanel(Context context) {
+        super(context);
+
+        mWindowManager = context.getSystemService(WindowManager.class);
+
+        mDensity = context.getResources().getDisplayMetrics().density;
+
+        mBaseTranslation = dp(BASE_TRANSLATION_DP);
+        mArrowLength = dp(ARROW_LENGTH_DP);
+        mArrowThickness = dp(ARROW_THICKNESS_DP);
+        mMinDeltaForSwitch = dp(32);
+
+        mPaint.setStrokeWidth(mArrowThickness);
+        mPaint.setStrokeCap(Paint.Cap.ROUND);
+        mPaint.setAntiAlias(true);
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setStrokeJoin(Paint.Join.ROUND);
+
+        mArrowDisappearAnimation = ValueAnimator.ofFloat(0.0f, 1.0f);
+        mArrowDisappearAnimation.setDuration(DISAPPEAR_ARROW_ANIMATION_DURATION_MS);
+        mArrowDisappearAnimation.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mArrowDisappearAnimation.addUpdateListener(animation -> {
+            mDisappearAmount = (float) animation.getAnimatedValue();
+            invalidate();
+        });
+
+        mAngleAnimation =
+                new SpringAnimation(this, CURRENT_ANGLE);
+        mAngleAppearForce = new SpringForce()
+                .setStiffness(500)
+                .setDampingRatio(0.5f);
+        mAngleDisappearForce = new SpringForce()
+                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
+                .setFinalPosition(90);
+        mAngleAnimation.setSpring(mAngleAppearForce).setMaxValue(90);
+
+        mTranslationAnimation =
+                new SpringAnimation(this, CURRENT_TRANSLATION);
+        mRegularTranslationSpring = new SpringForce()
+                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+        mTriggerBackSpring = new SpringForce()
+                .setStiffness(450)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+        mTranslationAnimation.setSpring(mRegularTranslationSpring);
+        mVerticalTranslationAnimation =
+                new SpringAnimation(this, CURRENT_VERTICAL_TRANSLATION);
+        mVerticalTranslationAnimation.setSpring(
+                new SpringForce()
+                        .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
+        mPaint.setColor(context.getColor(R.color.back_arrow_color_dark));
+        loadDimens();
+        updateArrowDirection();
+
+        mSwipeThreshold = ResourceUtils.getDimenByName(
+            "navigation_edge_action_drag_threshold", context.getResources(), 16 /* defaultValue */);
+        setVisibility(GONE);
+    }
+
+    void onDestroy() {
+        mWindowManager.removeView(this);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    @SuppressLint("RtlHardcoded")
+    void setIsLeftPanel(boolean isLeftPanel) {
+        mIsLeftPanel = isLeftPanel;
+        mLayoutParams.gravity = mIsLeftPanel
+                ? (Gravity.LEFT | Gravity.TOP)
+                : (Gravity.RIGHT | Gravity.TOP);
+    }
+
+    boolean getIsLeftPanel() {
+        return mIsLeftPanel;
+    }
+
+    void setDisplaySize(Point displaySize) {
+        mDisplaySize.set(displaySize.x, displaySize.y);
+        mScreenSize = Math.min(mDisplaySize.x, mDisplaySize.y);
+    }
+
+    void setBackCallback(BackCallback callback) {
+        mBackCallback = callback;
+    }
+
+    void setLayoutParams(WindowManager.LayoutParams layoutParams) {
+        mLayoutParams = layoutParams;
+        mWindowManager.addView(this, mLayoutParams);
+    }
+
+    private float getCurrentAngle() {
+        return mCurrentAngle;
+    }
+
+    private float getCurrentTranslation() {
+        return mCurrentTranslation;
+    }
+
+    void onMotionEvent(MotionEvent event) {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(event);
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                mDragSlopPassed = false;
+                resetOnDown();
+                mStartX = event.getX();
+                mStartY = event.getY();
+                setVisibility(VISIBLE);
+                updatePosition(event.getY());
+                mWindowManager.updateViewLayout(this, mLayoutParams);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                handleMoveEvent(event);
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mTriggerBack) {
+                    triggerBack();
+                } else {
+                    cancelBack();
+                }
+                mVelocityTracker.recycle();
+                mVelocityTracker = null;
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                cancelBack();
+                mVelocityTracker.recycle();
+                mVelocityTracker = null;
+                break;
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        updateArrowDirection();
+        loadDimens();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        float pointerPosition = mCurrentTranslation - mArrowThickness / 2.0f;
+        canvas.save();
+        canvas.translate(
+                mIsLeftPanel ? pointerPosition : getWidth() - pointerPosition,
+                (getHeight() * 0.5f) + mVerticalTranslation);
+
+        // Let's calculate the position of the end based on the angle
+        float x = (polarToCartX(mCurrentAngle) * mArrowLength);
+        float y = (polarToCartY(mCurrentAngle) * mArrowLength);
+        Path arrowPath = calculatePath(x, y);
+
+        canvas.drawPath(arrowPath, mPaint);
+        canvas.restore();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        mMaxTranslation = getWidth() - mArrowPaddingEnd;
+    }
+
+    private void loadDimens() {
+        Resources res = getResources();
+        mArrowPaddingEnd = ResourceUtils.getDimenByName("navigation_edge_panel_padding", res, 8);
+        mMinArrowPosition = ResourceUtils.getDimenByName("navigation_edge_arrow_min_y", res, 64);
+        mFingerOffset = ResourceUtils.getDimenByName("navigation_edge_finger_offset", res, 48);
+    }
+
+    private void updateArrowDirection() {
+        // Both panels arrow point the same way
+        mArrowsPointLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR;
+        invalidate();
+    }
+
+    private float getStaticArrowWidth() {
+        return polarToCartX(ARROW_ANGLE_WHEN_EXTENDED_DEGREES) * mArrowLength;
+    }
+
+    private float polarToCartX(float angleInDegrees) {
+        return (float) Math.cos(Math.toRadians(angleInDegrees));
+    }
+
+    private float polarToCartY(float angleInDegrees) {
+        return (float) Math.sin(Math.toRadians(angleInDegrees));
+    }
+
+    private Path calculatePath(float x, float y) {
+        if (!mArrowsPointLeft) {
+            x = -x;
+        }
+        float extent = lerp(1.0f, 0.75f, mDisappearAmount);
+        x = x * extent;
+        y = y * extent;
+        mArrowPath.reset();
+        mArrowPath.moveTo(x, y);
+        mArrowPath.lineTo(0, 0);
+        mArrowPath.lineTo(x, -y);
+        return mArrowPath;
+    }
+
+    private static float lerp(float start, float stop, float amount) {
+        return start + (stop - start) * amount;
+    }
+
+    private void triggerBack() {
+        if (mBackCallback != null) {
+            mBackCallback.triggerBack();
+        }
+
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.computeCurrentVelocity(1000);
+        // Only do the extra translation if we're not already flinging
+        boolean isSlow = Math.abs(mVelocityTracker.getXVelocity()) < 500;
+        if (isSlow
+                || SystemClock.uptimeMillis() - mVibrationTime >= GESTURE_DURATION_FOR_CLICK_MS) {
+            VibratorWrapper.INSTANCE.get(getContext()).vibrate(VibratorWrapper.EFFECT_CLICK);
+        }
+
+        // Let's also snap the angle a bit
+        if (mAngleOffset > -4) {
+            mAngleOffset = Math.max(-8, mAngleOffset - 8);
+            updateAngle(true /* animated */);
+        }
+
+        // Finally, after the translation, animate back and disappear the arrow
+        Runnable translationEnd = () -> {
+            // let's snap it back
+            mAngleOffset = Math.max(0, mAngleOffset + 8);
+            updateAngle(true /* animated */);
+
+            mTranslationAnimation.setSpring(mTriggerBackSpring);
+            // Translate the arrow back a bit to make for a nice transition
+            setDesiredTranslation(mDesiredTranslation - dp(32), true /* animated */);
+            animate().alpha(0f).setDuration(DISAPPEAR_FADE_ANIMATION_DURATION_MS)
+                    .withEndAction(() -> setVisibility(GONE));
+            mArrowDisappearAnimation.start();
+        };
+        if (mTranslationAnimation.isRunning()) {
+            mTranslationAnimation.addEndListener(new DynamicAnimation.OnAnimationEndListener() {
+                @Override
+                public void onAnimationEnd(DynamicAnimation animation, boolean canceled,
+                        float value,
+                        float velocity) {
+                    animation.removeEndListener(this);
+                    if (!canceled) {
+                        translationEnd.run();
+                    }
+                }
+            });
+        } else {
+            translationEnd.run();
+        }
+    }
+
+    private void cancelBack() {
+        if (mBackCallback != null) {
+            mBackCallback.cancelBack();
+        }
+
+        if (mTranslationAnimation.isRunning()) {
+            mTranslationAnimation.addEndListener(mSetGoneEndListener);
+        } else {
+            setVisibility(GONE);
+        }
+    }
+
+    private void resetOnDown() {
+        animate().cancel();
+        mAngleAnimation.cancel();
+        mTranslationAnimation.cancel();
+        mVerticalTranslationAnimation.cancel();
+        mArrowDisappearAnimation.cancel();
+        mAngleOffset = 0;
+        mTranslationAnimation.setSpring(mRegularTranslationSpring);
+        // Reset the arrow to the side
+        setTriggerBack(false /* triggerBack */, false /* animated */);
+        setDesiredTranslation(0, false /* animated */);
+        setCurrentTranslation(0);
+        updateAngle(false /* animate */);
+        mPreviousTouchTranslation = 0;
+        mTotalTouchDelta = 0;
+        mVibrationTime = 0;
+        setDesiredVerticalTransition(0, false /* animated */);
+    }
+
+    private void handleMoveEvent(MotionEvent event) {
+        float x = event.getX();
+        float y = event.getY();
+        float touchTranslation = Math.abs(x - mStartX);
+        float yOffset = y - mStartY;
+        float delta = touchTranslation - mPreviousTouchTranslation;
+        if (Math.abs(delta) > 0) {
+            if (Math.signum(delta) == Math.signum(mTotalTouchDelta)) {
+                mTotalTouchDelta += delta;
+            } else {
+                mTotalTouchDelta = delta;
+            }
+        }
+        mPreviousTouchTranslation = touchTranslation;
+
+        // Apply a haptic on drag slop passed
+        if (!mDragSlopPassed && touchTranslation > mSwipeThreshold) {
+            mDragSlopPassed = true;
+            VibratorWrapper.INSTANCE.get(getContext()).vibrate(VibratorWrapper.EFFECT_CLICK);
+            mVibrationTime = SystemClock.uptimeMillis();
+
+            // Let's show the arrow and animate it in!
+            mDisappearAmount = 0.0f;
+            setAlpha(1f);
+            // And animate it go to back by default!
+            setTriggerBack(true /* triggerBack */, true /* animated */);
+        }
+
+        // Let's make sure we only go to the baseextend and apply rubberbanding afterwards
+        if (touchTranslation > mBaseTranslation) {
+            float diff = touchTranslation - mBaseTranslation;
+            float progress = MathUtils.clamp(diff / (mScreenSize - mBaseTranslation), 0, 1);
+            progress = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
+                    * (mMaxTranslation - mBaseTranslation);
+            touchTranslation = mBaseTranslation + progress;
+        } else {
+            float diff = mBaseTranslation - touchTranslation;
+            float progress = MathUtils.clamp(diff / mBaseTranslation, 0, 1);
+            progress = RUBBER_BAND_INTERPOLATOR_APPEAR.getInterpolation(progress)
+                    * (mBaseTranslation / RUBBER_BAND_AMOUNT_APPEAR);
+            touchTranslation = mBaseTranslation - progress;
+        }
+        // By default we just assume the current direction is kept
+        boolean triggerBack = mTriggerBack;
+
+        //  First lets see if we had continuous motion in one direction for a while
+        if (Math.abs(mTotalTouchDelta) > mMinDeltaForSwitch) {
+            triggerBack = mTotalTouchDelta > 0;
+        }
+
+        // Then, let's see if our velocity tells us to change direction
+        mVelocityTracker.computeCurrentVelocity(1000);
+        float xVelocity = mVelocityTracker.getXVelocity();
+        float yVelocity = mVelocityTracker.getYVelocity();
+        float velocity = (float) Math.hypot(xVelocity, yVelocity);
+        mAngleOffset = Math.min(velocity / 1000 * ARROW_ANGLE_ADDED_PER_1000_SPEED,
+                ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES) * Math.signum(xVelocity);
+        if (mIsLeftPanel && mArrowsPointLeft || !mIsLeftPanel && !mArrowsPointLeft) {
+            mAngleOffset *= -1;
+        }
+
+        // Last if the direction in Y is bigger than X * 2 we also abort
+        if (Math.abs(yOffset) > Math.abs(x - mStartX) * 2) {
+            triggerBack = false;
+        }
+        setTriggerBack(triggerBack, true /* animated */);
+
+        if (!mTriggerBack) {
+            touchTranslation = 0;
+        } else if (mIsLeftPanel && mArrowsPointLeft
+                || (!mIsLeftPanel && !mArrowsPointLeft)) {
+            // If we're on the left we should move less, because the arrow is facing the other
+            // direction
+            touchTranslation -= getStaticArrowWidth();
+        }
+        setDesiredTranslation(touchTranslation, true /* animated */);
+        updateAngle(true /* animated */);
+
+        float maxYOffset = getHeight() / 2.0f - mArrowLength;
+        float progress =
+                MathUtils.clamp(Math.abs(yOffset) / (maxYOffset * RUBBER_BAND_AMOUNT), 0, 1);
+        float verticalTranslation = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
+                * maxYOffset * Math.signum(yOffset);
+        setDesiredVerticalTransition(verticalTranslation, true /* animated */);
+    }
+
+    private void updatePosition(float touchY) {
+        float position = touchY - mFingerOffset;
+        position = Math.max(position, mMinArrowPosition);
+        position -= mLayoutParams.height / 2.0f;
+        mLayoutParams.y = MathUtils.clamp((int) position, 0, mDisplaySize.y);
+    }
+
+    private void setDesiredVerticalTransition(float verticalTranslation, boolean animated) {
+        if (mDesiredVerticalTranslation != verticalTranslation) {
+            mDesiredVerticalTranslation = verticalTranslation;
+            if (!animated) {
+                setVerticalTranslation(verticalTranslation);
+            } else {
+                mVerticalTranslationAnimation.animateToFinalPosition(verticalTranslation);
+            }
+            invalidate();
+        }
+    }
+
+    private void setVerticalTranslation(float verticalTranslation) {
+        mVerticalTranslation = verticalTranslation;
+        invalidate();
+    }
+
+    private float getVerticalTranslation() {
+        return mVerticalTranslation;
+    }
+
+    private void setDesiredTranslation(float desiredTranslation, boolean animated) {
+        if (mDesiredTranslation != desiredTranslation) {
+            mDesiredTranslation = desiredTranslation;
+            if (!animated) {
+                setCurrentTranslation(desiredTranslation);
+            } else {
+                mTranslationAnimation.animateToFinalPosition(desiredTranslation);
+            }
+        }
+    }
+
+    private void setCurrentTranslation(float currentTranslation) {
+        mCurrentTranslation = currentTranslation;
+        invalidate();
+    }
+
+    private void setTriggerBack(boolean triggerBack, boolean animated) {
+        if (mTriggerBack != triggerBack) {
+            mTriggerBack = triggerBack;
+            mAngleAnimation.cancel();
+            updateAngle(animated);
+            // Whenever the trigger back state changes the existing translation animation should be
+            // cancelled
+            mTranslationAnimation.cancel();
+        }
+    }
+
+    private void updateAngle(boolean animated) {
+        float newAngle = mTriggerBack ? ARROW_ANGLE_WHEN_EXTENDED_DEGREES + mAngleOffset : 90;
+        if (newAngle != mDesiredAngle) {
+            if (!animated) {
+                setCurrentAngle(newAngle);
+            } else {
+                mAngleAnimation.setSpring(mTriggerBack ? mAngleAppearForce : mAngleDisappearForce);
+                mAngleAnimation.animateToFinalPosition(newAngle);
+            }
+            mDesiredAngle = newAngle;
+        }
+    }
+
+    private void setCurrentAngle(float currentAngle) {
+        mCurrentAngle = currentAngle;
+        invalidate();
+    }
+
+    private float dp(float dp) {
+        return mDensity * dp;
+    }
+
+    /** Callback to let the gesture handler react to the detected back gestures. */
+    interface BackCallback {
+        /** Indicates that a Back gesture was recognized. */
+        void triggerBack();
+
+        /** Indicates that the gesture was cancelled. */
+        void cancelBack();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
index 8081ad7..4815366 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -30,12 +30,11 @@
 import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
 
 import java.util.List;
-import java.util.Optional;
 
 /** Shows the gesture interactive sandbox in full screen mode. */
 public class GestureSandboxActivity extends FragmentActivity {
 
-    Optional<BackGestureTutorialFragment> mFragment = Optional.empty();
+    private BackGestureTutorialFragment mFragment;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -43,10 +42,10 @@
         requestWindowFeature(Window.FEATURE_NO_TITLE);
         setContentView(R.layout.back_gesture_tutorial_activity);
 
-        mFragment = Optional.of(BackGestureTutorialFragment.newInstance(TutorialStep.ENGAGED,
-                TutorialType.RIGHT_EDGE_BACK_NAVIGATION));
+        mFragment = BackGestureTutorialFragment.newInstance(
+            TutorialStep.ENGAGED, TutorialType.RIGHT_EDGE_BACK_NAVIGATION);
         getSupportFragmentManager().beginTransaction()
-                .add(R.id.back_gesture_tutorial_fragment_container, mFragment.get())
+                .add(R.id.back_gesture_tutorial_fragment_container, mFragment)
                 .commit();
     }
 
@@ -54,6 +53,13 @@
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
         disableSystemGestures();
+        mFragment.onAttachedToWindow();
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mFragment.onDetachedFromWindow();
     }
 
     @Override
@@ -64,13 +70,6 @@
         }
     }
 
-    @Override
-    public void onBackPressed() {
-        if (mFragment.isPresent()) {
-            mFragment.get().onBackPressed();
-        }
-    }
-
     private void hideSystemUI() {
         getWindow().getDecorView().setSystemUiVisibility(
                 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
diff --git a/res/layout/work_mode_switch.xml b/res/layout/work_mode_switch.xml
new file mode 100644
index 0000000..9cb7ce8
--- /dev/null
+++ b/res/layout/work_mode_switch.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.launcher3.allapps.WorkModeSwitch
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    style="@style/PrimaryMediumText"
+    android:id="@+id/work_mode_toggle"
+    android:drawableStart="@drawable/ic_corp"
+    android:drawablePadding="16dp"
+    android:drawableTint="?attr/workProfileOverlayTextColor"
+    android:textColor="?attr/workProfileOverlayTextColor"
+    android:layout_alignParentBottom="true"
+    android:ellipsize="end"
+    android:gravity="start"
+    android:lines="1"
+    android:showText="false"
+    android:textSize="16sp"
+    android:background="?attr/allAppsScrimColor"
+    android:text="@string/work_profile_toggle_label"
+    android:paddingBottom="@dimen/all_apps_work_profile_tab_footer_padding"
+    android:paddingLeft="@dimen/all_apps_work_profile_tab_footer_padding"
+    android:paddingRight="@dimen/all_apps_work_profile_tab_footer_padding"
+    android:paddingTop="@dimen/all_apps_work_profile_tab_footer_padding"
+/>
diff --git a/res/layout/work_tab_footer.xml b/res/layout/work_tab_footer.xml
deleted file mode 100644
index dbcdbdb..0000000
--- a/res/layout/work_tab_footer.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.launcher3.views.WorkFooterContainer
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:id="@+id/work_toggle_container"
-    android:focusable="true"
-    android:orientation="horizontal"
-    android:background="?attr/allAppsScrimColor"
-    android:paddingBottom="@dimen/all_apps_work_profile_tab_footer_padding"
-    android:paddingLeft="@dimen/all_apps_work_profile_tab_footer_padding"
-    android:paddingRight="@dimen/all_apps_work_profile_tab_footer_padding"
-    android:paddingTop="@dimen/all_apps_work_profile_tab_footer_padding">
-
-    <TextView
-        style="@style/PrimaryMediumText"
-        android:id="@+id/work_mode_label"
-        android:layout_width="0dp"
-        android:layout_weight="1"
-        android:drawableStart="@drawable/ic_corp"
-        android:drawablePadding="16dp"
-        android:drawableTint="?attr/workProfileOverlayTextColor"
-        android:textColor="?attr/workProfileOverlayTextColor"
-        android:layout_height="wrap_content"
-        android:ellipsize="end"
-        android:gravity="center_vertical"
-        android:lines="1"
-        android:minHeight="24dp"
-        android:paddingEnd="12dp"
-        android:textSize="16sp"/>
-    <com.android.launcher3.allapps.WorkModeSwitch
-        android:id="@+id/work_mode_toggle"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"/>
-
-</com.android.launcher3.views.WorkFooterContainer>
\ No newline at end of file
diff --git a/res/values/config.xml b/res/values/config.xml
index 6c239bd..1675a98 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -176,4 +176,7 @@
         <item>@dimen/swipe_up_fling_min_visible_change</item>
         <item>@dimen/swipe_up_y_overshoot</item>
     </array>
+
+    <string-array name="live_wallpapers_remove_sysui_scrims">
+    </string-array>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7748b3a..b1077be 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -328,22 +328,21 @@
     <!-- This string is in the work profile tab when a user has All Apps open on their phone. This is a label for a toggle to turn the work profile on and off. "Work profile" means a separate profile on a user's phone that's specifically for their work apps and managed by their company. "Work" is used as an adjective.-->
     <string name="work_profile_toggle_label">Work profile</string>
     <!--- User onboarding title for personal apps -->
-    <string name="work_profile_edu_personal_apps">Personal apps are private &amp; can\'t be seen by IT</string>
+    <string name="work_profile_edu_personal_apps">Personal data is separate &amp; hidden from work apps</string>
     <!--- User onboarding title for work profile apps -->
-    <string name="work_profile_edu_work_apps">Work apps are badged &amp; visible to IT</string>
+    <string name="work_profile_edu_work_apps">Work apps &amp; data are visible to your IT admin</string>
     <!-- Action label to proceed to the next work profile edu section-->
     <string name="work_profile_edu_next">Next</string>
     <!-- Action label to finish work profile edu-->
     <string name="work_profile_edu_accept">Got it</string>
 
-    <!-- This string is in the work profile tab when a user has All Apps open on their phone. It describes the label of a toggle, "Work profile," as being managed by the user's employer.
-    "Organization" is used to represent a variety of businesses, non-profits, and educational institutions).-->
-    <string name="work_mode_on_label">Work apps: On</string>
-    <!-- This string appears under a the label of a toggle in the work profile tab on a user's phone. It describes the status of the toggle, "Work profile," when it's turned off. "Work profile" means a separate profile on a user's phone that's speficially for their work apps and is managed by their company.-->
-    <string name="work_mode_off_label">Work apps: Paused</string>
+    <!--- heading shown when user opens work apps tab while work apps are paused -->
+    <string name="work_apps_paused_title">Work profile is paused</string>
+    <!--- body shown when user opens work apps tab while work apps are paused -->
+    <string name="work_apps_paused_body">Work apps can\’t send you notifications, use your battery, or access your location</string>
 
-    <string name="work_apps_paused_title">Work apps are paused</string>
-    <string name="work_apps_paused_body">You won\'t get any work notifications, and your IT admin can\'t see your location</string>
+    <!-- A tip shown pointing at work toggle -->
+    <string name="work_switch_tip">Pause work apps and notifications</string>
 
     <!-- Failed action error message: e.g. Failed: Pause -->
     <string name="remote_action_failed">Failed: <xliff:g id="what" example="Pause">%1$s</xliff:g></string>
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 12b5fc1..bed8278 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -180,7 +180,10 @@
         return null;
     }
 
-    protected static <T extends AbstractFloatingView> T getOpenView(
+    /**
+     * Returns a view matching FloatingViewType
+     */
+    public static <T extends AbstractFloatingView> T getOpenView(
             ActivityContext activity, @FloatingViewType int type) {
         BaseDragLayer dragLayer = activity.getDragLayer();
         if (dragLayer == null) return null;
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index 2b0da43..34d7067 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -155,7 +155,7 @@
 
     @Override
     public final void onDragEnter(DragObject d) {
-        if (!d.accessibleDrag && !mTextVisible) {
+        if (!mAccessibleDrag && !mTextVisible) {
             // Show tooltip
             hideTooltip();
 
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 6c562cf..9682d09 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -53,8 +53,6 @@
 
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
-import com.android.launcher3.accessibility.FolderAccessibilityHelper;
-import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PropertyListBuilder;
 import com.android.launcher3.config.FeatureFlags;
@@ -80,9 +78,6 @@
 import java.util.Stack;
 
 public class CellLayout extends ViewGroup implements Transposable {
-    public static final int WORKSPACE_ACCESSIBILITY_DRAG = 2;
-    public static final int FOLDER_ACCESSIBILITY_DRAG = 1;
-
     private static final String TAG = "CellLayout";
     private static final boolean LOGD = false;
 
@@ -188,7 +183,6 @@
     private static final Paint sPaint = new Paint();
 
     // Related to accessible drag and drop
-    private DragAndDropAccessibilityDelegate mTouchHelper;
     private boolean mUseTouchHelper = false;
     private RotationMode mRotationMode = RotationMode.NORMAL;
 
@@ -298,26 +292,20 @@
         addView(mShortcutsAndWidgets);
     }
 
-    public void enableAccessibleDrag(boolean enable, int dragType) {
-        mUseTouchHelper = enable;
-        if (!enable) {
-            ViewCompat.setAccessibilityDelegate(this, null);
-            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
-            getShortcutsAndWidgets().setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
-            setOnClickListener(null);
-        } else {
-            if (dragType == WORKSPACE_ACCESSIBILITY_DRAG &&
-                    !(mTouchHelper instanceof WorkspaceAccessibilityHelper)) {
-                mTouchHelper = new WorkspaceAccessibilityHelper(this);
-            } else if (dragType == FOLDER_ACCESSIBILITY_DRAG &&
-                    !(mTouchHelper instanceof FolderAccessibilityHelper)) {
-                mTouchHelper = new FolderAccessibilityHelper(this);
-            }
-            ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
-            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
-            getShortcutsAndWidgets().setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
-            setOnClickListener(mTouchHelper);
-        }
+
+    /**
+     * Sets or clears a delegate used for accessible drag and drop
+     */
+    public void setDragAndDropAccessibilityDelegate(DragAndDropAccessibilityDelegate delegate) {
+        setOnClickListener(delegate);
+        setOnHoverListener(delegate);
+        ViewCompat.setAccessibilityDelegate(this, delegate);
+
+        mUseTouchHelper = delegate != null;
+        int accessibilityFlag = mUseTouchHelper
+                ? IMPORTANT_FOR_ACCESSIBILITY_YES : IMPORTANT_FOR_ACCESSIBILITY_NO;
+        setImportantForAccessibility(accessibilityFlag);
+        getShortcutsAndWidgets().setImportantForAccessibility(accessibilityFlag);
 
         // Invalidate the accessibility hierarchy
         if (getParent() != null) {
@@ -345,15 +333,6 @@
     }
 
     @Override
-    public boolean dispatchHoverEvent(MotionEvent event) {
-        // Always attempt to dispatch hover events to accessibility first.
-        if (mUseTouchHelper && mTouchHelper.dispatchHoverEvent(event)) {
-            return true;
-        }
-        return super.dispatchHoverEvent(event);
-    }
-
-    @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         if (mUseTouchHelper ||
                 (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev))) {
@@ -2802,7 +2781,6 @@
      * Finds solution to accept hotseat migration to cell layout. commits solution if commitConfig
      */
     public boolean makeSpaceForHotseatMigration(boolean commitConfig) {
-        if (FeatureFlags.HOTSEAT_MIGRATE_NEW_PAGE.get()) return false;
         int[] cellPoint = new int[2];
         int[] directionVector = new int[]{0, -1};
         cellToPoint(0, mCountY, cellPoint);
@@ -2812,12 +2790,23 @@
             if (commitConfig) {
                 copySolutionToTempState(configuration, null);
                 commitTempPlacement();
+                // undo marking cells occupied since there is actually nothing being placed yet.
+                mOccupied.markCells(0, mCountY - 1, mCountX, 1, false);
             }
             return true;
         }
         return false;
     }
 
+    /**
+     * returns a copy of cell layout's grid occupancy
+     */
+    public GridOccupancy cloneGridOccupancy() {
+        GridOccupancy occupancy = new GridOccupancy(mCountX, mCountY);
+        mOccupied.copyTo(occupancy);
+        return occupancy;
+    }
+
     public boolean isRegionVacant(int x, int y, int spanX, int spanY) {
         return mOccupied.isRegionVacant(x, y, spanX, spanY);
     }
diff --git a/src/com/android/launcher3/DropTarget.java b/src/com/android/launcher3/DropTarget.java
index 43ec5a6..ef02e87 100644
--- a/src/com/android/launcher3/DropTarget.java
+++ b/src/com/android/launcher3/DropTarget.java
@@ -60,9 +60,6 @@
         /** Where the drag originated */
         public DragSource dragSource = null;
 
-        /** The object is part of an accessible drag operation */
-        public boolean accessibleDrag;
-
         /** Indicates that the drag operation was cancelled */
         public boolean cancelled = false;
 
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 2ad84b9..7414a88 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -162,9 +162,7 @@
                     "PreviewContext is passed into this IDP constructor");
         }
 
-        String gridName = Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)
-                ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null)
-                : null;
+        String gridName = getCurrentGridName(context);
         initGrid(context, gridName);
         mConfigMonitor = new ConfigMonitor(context,
                 APPLY_CONFIG_AT_RUNTIME.get() ? this::onConfigChanged : this::killProcess);
@@ -188,6 +186,12 @@
         initGrid(context, null, new Info(display));
     }
 
+    public static String getCurrentGridName(Context context) {
+        return Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)
+                ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null)
+                : null;
+    }
+
     /**
      * Retrieve system defined or RRO overriden icon shape.
      */
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 1413a5c..a83a694 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -124,7 +124,7 @@
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.AllAppsSwipeController;
 import com.android.launcher3.touch.ItemClickHandler;
-import com.android.launcher3.uioverrides.BackgroundBlurController;
+import com.android.launcher3.uioverrides.DepthController;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -326,19 +326,21 @@
     private boolean mDeferOverlayCallbacks;
     private final Runnable mDeferredOverlayCallbacks = this::checkIfOverlayStillDeferred;
 
-    private BackgroundBlurController mBackgroundBlurController =
-            new BackgroundBlurController(this);
+    private DepthController mDepthController =
+            new DepthController(this);
 
     private final ViewTreeObserver.OnDrawListener mOnDrawListener =
             new ViewTreeObserver.OnDrawListener() {
                 @Override
                 public void onDraw() {
-                    getBackgroundBlurController().setSurfaceToLauncher(mDragLayer);
+                    getDepthController().setSurfaceToLauncher(mDragLayer);
                     mDragLayer.post(() -> mDragLayer.getViewTreeObserver().removeOnDrawListener(
                             this));
                 }
             };
 
+    private long mLastTouchUpTime = -1;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         Object traceToken = TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT,
@@ -954,7 +956,7 @@
 
         NotificationListener.removeNotificationsChangedListener();
         getStateManager().moveToRestState();
-        getBackgroundBlurController().setSurfaceToLauncher(null);
+        getDepthController().setSurfaceToLauncher(null);
 
         // Workaround for b/78520668, explicitly trim memory once UI is hidden
         onTrimMemory(TRIM_MEMORY_UI_HIDDEN);
@@ -1115,7 +1117,7 @@
 
         super.onPause();
         mDragController.cancelDrag();
-        mDragController.resetLastGestureUpTime();
+        mLastTouchUpTime = -1;
         mDropTargetBar.animateToVisibility(false);
 
         if (!mDeferOverlayCallbacks) {
@@ -1838,6 +1840,9 @@
 
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_UP) {
+            mLastTouchUpTime = System.currentTimeMillis();
+        }
         TestLogging.recordMotionEvent(TestProtocol.SEQUENCE_MAIN, "Touch event", ev);
         return super.dispatchTouchEvent(ev);
     }
@@ -2465,8 +2470,12 @@
     }
 
     private boolean canRunNewAppsAnimation() {
-        long diff = System.currentTimeMillis() - mDragController.getLastGestureUpTime();
-        return diff > (NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS * 1000);
+        if (mDragController.isDragging()) {
+            return false;
+        } else {
+            return (System.currentTimeMillis() - mLastTouchUpTime)
+                    > (NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS * 1000);
+        }
     }
 
     private ValueAnimator createNewAppBounceAnimation(View v, int i) {
@@ -2708,7 +2717,7 @@
 
     protected StateHandler[] createStateHandlers() {
         return new StateHandler[] { getAllAppsController(), getWorkspace(),
-                getBackgroundBlurController() };
+                getDepthController() };
     }
 
     public TouchController[] createTouchControllers() {
@@ -2745,8 +2754,8 @@
         return Stream.of(APP_INFO, WIDGETS, INSTALL);
     }
 
-    public BackgroundBlurController getBackgroundBlurController() {
-        return mBackgroundBlurController;
+    public DepthController getDepthController() {
+        return mDepthController;
     }
 
     public static Launcher getLauncher(Context context) {
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index df71f16..6ee82cd 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -271,11 +271,13 @@
     }
 
     /**
-     * The amount of blur to apply to the background of either the app or Launcher surface in this
-     * state.
+     * The amount of blur and wallpaper zoom to apply to the background of either the app
+     * or Launcher surface in this state. Should be a number between 0 and 1, inclusive.
+     *
+     * 0 means completely zoomed in, without blurs. 1 is zoomed out, with blurs.
      */
-    public int getBackgroundBlurRadius(Context context) {
-        return 0;
+    public float getDepth(Context context) {
+        return 0f;
     }
 
     public String getDescription(Launcher launcher) {
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index d551776..c42e480 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -192,10 +192,7 @@
     final WallpaperOffsetInterpolator mWallpaperOffset;
     private boolean mUnlockWallpaperFromDefaultPageOnLayout;
 
-    // Variables relating to the creation of user folders by hovering shortcuts over shortcuts
-    private static final int FOLDER_CREATION_TIMEOUT = 0;
     public static final int REORDER_TIMEOUT = 650;
-    private final Alarm mFolderCreationAlarm = new Alarm();
     private final Alarm mReorderAlarm = new Alarm();
     private PreviewBackground mFolderCreateBg;
     private FolderIcon mDragOverFolderIcon = null;
@@ -567,11 +564,6 @@
         addView(newScreen, insertIndex);
         mStateTransitionAnimation.applyChildState(
                 mLauncher.getStateManager().getState(), newScreen, insertIndex);
-
-        if (mLauncher.getAccessibilityDelegate().isInAccessibleDrag()) {
-            newScreen.enableAccessibleDrag(true, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG);
-        }
-
         return newScreen;
     }
 
@@ -818,11 +810,6 @@
                 if (indexOfChild(cl) < currentPage) {
                     pageShift++;
                 }
-
-                if (isInAccessibleDrag) {
-                    cl.enableAccessibleDrag(false, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG);
-                }
-
                 removeView(cl);
             } else {
                 // if this is the last screen, convert it to the empty screen
@@ -1444,14 +1431,14 @@
         child.setVisibility(INVISIBLE);
 
         if (options.isAccessibleDrag) {
-            mDragController.addDragListener(new AccessibleDragListenerAdapter(
-                    this, CellLayout.WORKSPACE_ACCESSIBILITY_DRAG) {
-                @Override
-                protected void enableAccessibleDrag(boolean enable) {
-                    super.enableAccessibleDrag(enable);
-                    setEnableForLayout(mLauncher.getHotseat(),enable);
-                }
-            });
+            mDragController.addDragListener(
+                    new AccessibleDragListenerAdapter(this, WorkspaceAccessibilityHelper::new) {
+                        @Override
+                        protected void enableAccessibleDrag(boolean enable) {
+                            super.enableAccessibleDrag(enable);
+                            setEnableForLayout(mLauncher.getHotseat(), enable);
+                        }
+                    });
         }
 
         beginDragShared(child, this, options);
@@ -1883,12 +1870,10 @@
                         final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell;
                         AppWidgetProviderInfo pInfo = hostView.getAppWidgetInfo();
                         if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE
-                                && !d.accessibleDrag) {
-                            onCompleteRunnable = new Runnable() {
-                                public void run() {
-                                    if (!isPageInTransition()) {
-                                        AppWidgetResizeFrame.showForWidget(hostView, cellLayout);
-                                    }
+                                && !options.isAccessibleDrag) {
+                            onCompleteRunnable = () -> {
+                                if (!isPageInTransition()) {
+                                    AppWidgetResizeFrame.showForWidget(hostView, cellLayout);
                                 }
                             };
                         }
@@ -2088,8 +2073,6 @@
         if (mFolderCreateBg != null) {
             mFolderCreateBg.animateToRest();
         }
-        mFolderCreationAlarm.setOnAlarmListener(null);
-        mFolderCreationAlarm.cancelAlarm();
     }
 
     private void cleanupAddToFolder() {
@@ -2196,7 +2179,7 @@
             float targetCellDistance = mDragTargetLayout.getDistanceFromCell(
                     mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell);
 
-            manageFolderFeedback(mDragTargetLayout, mTargetCell, targetCellDistance, d);
+            manageFolderFeedback(targetCellDistance, d);
 
             boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int)
                     mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
@@ -2294,8 +2277,7 @@
         return null;
     }
 
-    private void manageFolderFeedback(CellLayout targetLayout,
-            int[] targetCell, float distance, DragObject dragObject) {
+    private void manageFolderFeedback(float distance, DragObject dragObject) {
         if (distance > mMaxDistanceForFolderCreation) {
             if ((mDragMode == DRAG_MODE_ADD_TO_FOLDER
                     || mDragMode == DRAG_MODE_CREATE_FOLDER)) {
@@ -2307,18 +2289,18 @@
         final View dragOverView = mDragTargetLayout.getChildAt(mTargetCell[0], mTargetCell[1]);
         ItemInfo info = dragObject.dragInfo;
         boolean userFolderPending = willCreateUserFolder(info, dragOverView, false);
-        if (mDragMode == DRAG_MODE_NONE && userFolderPending &&
-                !mFolderCreationAlarm.alarmPending()) {
+        if (mDragMode == DRAG_MODE_NONE && userFolderPending) {
 
-            FolderCreationAlarmListener listener = new
-                    FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1]);
+            mFolderCreateBg = new PreviewBackground();
+            mFolderCreateBg.setup(mLauncher, mLauncher, null,
+                    dragOverView.getMeasuredWidth(), dragOverView.getPaddingTop());
 
-            if (!dragObject.accessibleDrag) {
-                mFolderCreationAlarm.setOnAlarmListener(listener);
-                mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT);
-            } else {
-                listener.onAlarm(mFolderCreationAlarm);
-            }
+            // The full preview background should appear behind the icon
+            mFolderCreateBg.isClipping = false;
+
+            mFolderCreateBg.animateToAccept(mDragTargetLayout, mTargetCell[0], mTargetCell[1]);
+            mDragTargetLayout.clearDragOutlines();
+            setDragMode(DRAG_MODE_CREATE_FOLDER);
 
             if (dragObject.stateAnnouncer != null) {
                 dragObject.stateAnnouncer.announce(WorkspaceAccessibilityHelper
@@ -2331,8 +2313,8 @@
         if (willAddToFolder && mDragMode == DRAG_MODE_NONE) {
             mDragOverFolderIcon = ((FolderIcon) dragOverView);
             mDragOverFolderIcon.onDragEnter(info);
-            if (targetLayout != null) {
-                targetLayout.clearDragOutlines();
+            if (mDragTargetLayout != null) {
+                mDragTargetLayout.clearDragOutlines();
             }
             setDragMode(DRAG_MODE_ADD_TO_FOLDER);
 
@@ -2351,33 +2333,6 @@
         }
     }
 
-    class FolderCreationAlarmListener implements OnAlarmListener {
-        final CellLayout layout;
-        final int cellX;
-        final int cellY;
-
-        final PreviewBackground bg = new PreviewBackground();
-
-        public FolderCreationAlarmListener(CellLayout layout, int cellX, int cellY) {
-            this.layout = layout;
-            this.cellX = cellX;
-            this.cellY = cellY;
-
-            BubbleTextView cell = (BubbleTextView) layout.getChildAt(cellX, cellY);
-            bg.setup(mLauncher, mLauncher, null, cell.getMeasuredWidth(), cell.getPaddingTop());
-
-            // The full preview background should appear behind the icon
-            bg.isClipping = false;
-        }
-
-        public void onAlarm(Alarm alarm) {
-            mFolderCreateBg = bg;
-            mFolderCreateBg.animateToAccept(layout, cellX, cellY);
-            layout.clearDragOutlines();
-            setDragMode(DRAG_MODE_CREATE_FOLDER);
-        }
-    }
-
     class ReorderAlarmListener implements OnAlarmListener {
         final float[] dragViewCenter;
         final int minSpanX, minSpanY, spanX, spanY;
diff --git a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
index f8df5d7..0d7df2b 100644
--- a/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
+++ b/src/com/android/launcher3/accessibility/AccessibleDragListenerAdapter.java
@@ -16,7 +16,9 @@
 
 package com.android.launcher3.accessibility;
 
+import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewGroup.OnHierarchyChangeListener;
 
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DropTarget.DragObject;
@@ -24,36 +26,55 @@
 import com.android.launcher3.dragndrop.DragController.DragListener;
 import com.android.launcher3.dragndrop.DragOptions;
 
+import java.util.function.Function;
+
 /**
  * Utility listener to enable/disable accessibility drag flags for a ViewGroup
  * containing CellLayouts
  */
-public class AccessibleDragListenerAdapter implements DragListener {
+public class AccessibleDragListenerAdapter implements DragListener, OnHierarchyChangeListener {
 
     private final ViewGroup mViewGroup;
-    private final int mDragType;
+    private final Function<CellLayout, DragAndDropAccessibilityDelegate> mDelegateFactory;
 
     /**
-     * @param parent
-     * @param dragType either {@link CellLayout#WORKSPACE_ACCESSIBILITY_DRAG} or
-     *                 {@link CellLayout#FOLDER_ACCESSIBILITY_DRAG}
+     * @param parent the viewgroup containing all the children
+     * @param delegateFactory function to create no delegates
      */
-    public AccessibleDragListenerAdapter(ViewGroup parent, int dragType) {
+    public AccessibleDragListenerAdapter(ViewGroup parent,
+            Function<CellLayout, DragAndDropAccessibilityDelegate> delegateFactory) {
         mViewGroup = parent;
-        mDragType = dragType;
+        mDelegateFactory = delegateFactory;
     }
 
     @Override
     public void onDragStart(DragObject dragObject, DragOptions options) {
+        mViewGroup.setOnHierarchyChangeListener(this);
         enableAccessibleDrag(true);
     }
 
     @Override
     public void onDragEnd() {
+        mViewGroup.setOnHierarchyChangeListener(null);
         enableAccessibleDrag(false);
         Launcher.getLauncher(mViewGroup.getContext()).getDragController().removeDragListener(this);
     }
 
+
+    @Override
+    public void onChildViewAdded(View parent, View child) {
+        if (parent == mViewGroup) {
+            setEnableForLayout((CellLayout) child, true);
+        }
+    }
+
+    @Override
+    public void onChildViewRemoved(View parent, View child) {
+        if (parent == mViewGroup) {
+            setEnableForLayout((CellLayout) child, false);
+        }
+    }
+
     protected void enableAccessibleDrag(boolean enable) {
         for (int i = 0; i < mViewGroup.getChildCount(); i++) {
             setEnableForLayout((CellLayout) mViewGroup.getChildAt(i), enable);
@@ -61,6 +82,6 @@
     }
 
     protected final void setEnableForLayout(CellLayout layout, boolean enable) {
-        layout.enableAccessibleDrag(enable, mDragType);
+        layout.setDragAndDropAccessibilityDelegate(enable ? mDelegateFactory.apply(layout) : null);
     }
 }
diff --git a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
index 117296d..ddb547f 100644
--- a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
@@ -19,24 +19,26 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.view.View.OnHoverListener;
 import android.view.accessibility.AccessibilityEvent;
 
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.customview.widget.ExploreByTouchHelper;
+
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 
 import java.util.List;
 
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
-import androidx.customview.widget.ExploreByTouchHelper;
-
 /**
  * Helper class to make drag-and-drop in a {@link CellLayout} accessible.
  */
 public abstract class DragAndDropAccessibilityDelegate extends ExploreByTouchHelper
-        implements OnClickListener {
+        implements OnClickListener, OnHoverListener {
     protected static final int INVALID_POSITION = -1;
 
     private static final int[] sTempArray = new int[2];
@@ -123,6 +125,11 @@
         node.setFocusable(true);
     }
 
+    @Override
+    public boolean onHover(View view, MotionEvent motionEvent) {
+        return dispatchHoverEvent(motionEvent);
+    }
+
     protected abstract String getLocationDescriptionForIconDrop(int id);
 
     protected abstract String getConfirmationForIconDrop(int id);
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 0b439ec..6f7f8e6 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -7,6 +7,7 @@
 import android.app.AlertDialog;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.DialogInterface;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Handler;
@@ -400,11 +401,11 @@
 
         Rect pos = new Rect();
         mLauncher.getDragLayer().getDescendantRectRelativeToSelf(item, pos);
-        mLauncher.getDragController().prepareAccessibleDrag(pos.centerX(), pos.centerY());
         mLauncher.getDragController().addDragListener(this);
 
         DragOptions options = new DragOptions();
         options.isAccessibleDrag = true;
+        options.simulatedDndStartPoint = new Point(pos.centerX(), pos.centerY());
         ItemLongClickListener.beginDrag(item, mLauncher, info, options);
     }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index e085ff0..10a3060 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -62,7 +62,6 @@
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 import com.android.launcher3.views.SpringRelativeLayout;
-import com.android.launcher3.views.WorkFooterContainer;
 
 import java.util.ArrayList;
 
@@ -91,7 +90,7 @@
     private AllAppsPagedView mViewPager;
 
     private FloatingHeaderView mHeader;
-    private WorkFooterContainer mWorkFooterContainer;
+    private WorkModeSwitch mWorkModeSwitch;
 
 
     private SpannableStringBuilder mSearchQueryBuilder = null;
@@ -156,8 +155,8 @@
         return mMultiValueAlpha.getProperty(index);
     }
 
-    public WorkFooterContainer getWorkFooterContainer() {
-        return mWorkFooterContainer;
+    public WorkModeSwitch getWorkModeSwitch() {
+        return mWorkModeSwitch;
     }
 
 
@@ -195,7 +194,7 @@
     }
 
     private void resetWorkProfile() {
-        mWorkFooterContainer.refresh();
+        mWorkModeSwitch.refresh();
         mAH[AdapterHolder.WORK].setupOverlay();
         mAH[AdapterHolder.WORK].applyPadding();
     }
@@ -410,9 +409,9 @@
         } else {
             mAH[AdapterHolder.MAIN].setup(findViewById(R.id.apps_list_view), null);
             mAH[AdapterHolder.WORK].recyclerView = null;
-            if (mWorkFooterContainer != null) {
-                ((ViewGroup) mWorkFooterContainer.getParent()).removeView(mWorkFooterContainer);
-                mWorkFooterContainer = null;
+            if (mWorkModeSwitch != null) {
+                ((ViewGroup) mWorkModeSwitch.getParent()).removeView(mWorkModeSwitch);
+                mWorkModeSwitch = null;
             }
         }
         setupHeader();
@@ -422,14 +421,11 @@
     }
 
     private void setupWorkToggle() {
-        mWorkFooterContainer = (WorkFooterContainer) mLauncher.getLayoutInflater().inflate(
-                R.layout.work_tab_footer, findViewById(R.id.work_toggle_container));
-        mWorkFooterContainer.setLayoutParams(
-                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                        ViewGroup.LayoutParams.WRAP_CONTENT));
-        this.addView(mWorkFooterContainer);
-        mWorkFooterContainer.setInsets(mInsets);
-        mWorkFooterContainer.post(() -> mAH[AdapterHolder.WORK].applyPadding());
+        mWorkModeSwitch = (WorkModeSwitch) mLauncher.getLayoutInflater().inflate(
+                R.layout.work_mode_switch, this, false);
+        this.addView(mWorkModeSwitch);
+        mWorkModeSwitch.setInsets(mInsets);
+        mWorkModeSwitch.post(() -> mAH[AdapterHolder.WORK].applyPadding());
     }
 
     private void replaceRVContainer(boolean showTabs) {
@@ -469,8 +465,8 @@
             findViewById(R.id.tab_work)
                     .setOnClickListener((View view) -> mViewPager.snapToPage(AdapterHolder.WORK));
         }
-        if (mWorkFooterContainer != null) {
-            mWorkFooterContainer.setWorkTabVisible(pos == AdapterHolder.WORK);
+        if (mWorkModeSwitch != null) {
+            mWorkModeSwitch.setWorkTabVisible(pos == AdapterHolder.WORK);
         }
     }
 
@@ -648,6 +644,8 @@
             if (!mIsWork || recyclerView == null) return;
             boolean workDisabled = UserCache.INSTANCE.get(mLauncher).isAnyProfileQuietModeEnabled();
             if (mWorkDisabled == workDisabled) return;
+            recyclerView.setContentDescription(
+                    workDisabled ? mLauncher.getString(R.string.work_apps_paused_title) : null);
             if (workDisabled) {
                 appsList.updateItemFilter((info, cn) -> false);
                 recyclerView.addAutoSizedOverlay(
@@ -662,8 +660,7 @@
         void applyPadding() {
             if (recyclerView != null) {
                 int bottomOffset =
-                        mWorkFooterContainer != null && mIsWork ? mWorkFooterContainer.getHeight()
-                                : 0;
+                        mWorkModeSwitch != null && mIsWork ? mWorkModeSwitch.getHeight() : 0;
                 recyclerView.setPadding(padding.left, padding.top, padding.right,
                         padding.bottom + bottomOffset);
             }
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index c4b2f68..a6ef10a 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.AppInfo.COMPONENT_KEY_COMPARATOR;
 import static com.android.launcher3.AppInfo.EMPTY_ARRAY;
 
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -55,6 +56,8 @@
     private int mDeferUpdatesFlags = 0;
     private boolean mUpdatePending = false;
 
+    private boolean mListenerUpdateInProgress = false;
+
     public AppInfo[] getApps() {
         return mApps;
     }
@@ -99,10 +102,12 @@
             mUpdatePending = true;
             return;
         }
+        mListenerUpdateInProgress = true;
         int count = mUpdateListeners.size();
         for (int i = 0; i < count; i++) {
             mUpdateListeners.get(i).onAppsUpdated();
         }
+        mListenerUpdateInProgress = false;
     }
 
     public void addUpdateListener(OnUpdateListener listener) {
@@ -110,6 +115,9 @@
     }
 
     public void removeUpdateListener(OnUpdateListener listener) {
+        if (mListenerUpdateInProgress) {
+            Log.e("AllAppsStore", "Trying to remove listener during update", new Exception());
+        }
         mUpdateListeners.remove(listener);
     }
 
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
index aadb297..f935e4d 100644
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018 The Android Open Source Project
+ * 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.
@@ -15,7 +15,13 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
+
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.Rect;
 import android.os.AsyncTask;
 import android.os.Process;
 import android.os.UserHandle;
@@ -24,28 +30,45 @@
 import android.view.MotionEvent;
 import android.widget.Switch;
 
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.pm.UserCache;
 
 import java.lang.ref.WeakReference;
 
-public class WorkModeSwitch extends Switch {
+/**
+ * Work profile toggle switch shown at the bottom of AllApps work tab
+ */
+public class WorkModeSwitch extends Switch implements Insettable {
+
+    private Rect mInsets = new Rect();
+    protected ObjectAnimator mOpenCloseAnimator;
+
 
     public WorkModeSwitch(Context context) {
         super(context);
+        init();
     }
 
     public WorkModeSwitch(Context context, AttributeSet attrs) {
         super(context, attrs);
+        init();
     }
 
     public WorkModeSwitch(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
+        init();
+    }
+
+    private void init() {
+        mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this);
     }
 
     @Override
     public void setChecked(boolean checked) {
-        // No-op, do not change the checked state until broadcast is received.
+
     }
 
     @Override
@@ -55,15 +78,24 @@
 
     private void setCheckedInternal(boolean checked) {
         super.setChecked(checked);
+        setCompoundDrawablesWithIntrinsicBounds(
+                checked ? R.drawable.ic_corp : R.drawable.ic_corp_off, 0, 0, 0);
     }
 
     public void refresh() {
+        if (!shouldShowWorkSwitch()) return;
         UserCache userManager = UserCache.INSTANCE.get(getContext());
         setCheckedInternal(!userManager.isAnyProfileQuietModeEnabled());
         setEnabled(true);
     }
 
     @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        this.setVisibility(shouldShowWorkSwitch() ? VISIBLE : GONE);
+    }
+
+    @Override
     public boolean onTouchEvent(MotionEvent ev) {
         return ev.getActionMasked() == MotionEvent.ACTION_MOVE || super.onTouchEvent(ev);
     }
@@ -72,6 +104,24 @@
         new SetQuietModeEnabledAsyncTask(enabled, new WeakReference<>(this)).execute();
     }
 
+    @Override
+    public void setInsets(Rect insets) {
+        int bottomInset = insets.bottom - mInsets.bottom;
+        mInsets.set(insets);
+        setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
+                getPaddingBottom() + bottomInset);
+    }
+
+    /**
+     * Animates in/out work profile toggle panel based on the tab user is on
+     */
+    public void setWorkTabVisible(boolean workTabVisible) {
+        if (!shouldShowWorkSwitch()) return;
+
+        mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat(ALPHA, workTabVisible ? 1 : 0));
+        mOpenCloseAnimator.start();
+    }
+
     private static final class SetQuietModeEnabledAsyncTask
             extends AsyncTask<Void, Void, Boolean> {
 
@@ -122,4 +172,11 @@
             }
         }
     }
+
+    private boolean shouldShowWorkSwitch() {
+        Launcher launcher = Launcher.getLauncher(getContext());
+        return Utilities.ATLEAST_P && (hasShortcutsPermission(launcher)
+                || launcher.checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
+                == PackageManager.PERMISSION_GRANTED);
+    }
 }
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 0237b50..4df3b0a 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -120,12 +120,11 @@
             "ASSISTANT_GIVES_LAUNCHER_FOCUS", false,
             "Allow Launcher to handle nav bar gestures while Assistant is running over it");
 
-    public static final BooleanFlag ENABLE_HYBRID_HOTSEAT = getDebugFlag(
+    public static final BooleanFlag ENABLE_HYBRID_HOTSEAT = new DeviceFlag(
             "ENABLE_HYBRID_HOTSEAT", false, "Fill gaps in hotseat with predicted apps");
 
-    public static final BooleanFlag HOTSEAT_MIGRATE_NEW_PAGE = getDebugFlag(
-            "HOTSEAT_MIGRATE_NEW_PAGE", false,
-            "Migrates hotseat to a new workspace page instead of same page");
+    public static final BooleanFlag HOTSEAT_MIGRATE_TO_FOLDER = new DeviceFlag(
+            "HOTSEAT_MIGRATE_TO_FOLDER", false, "Should move hotseat items into a folder");
 
     public static final BooleanFlag ENABLE_DEEP_SHORTCUT_ICON_CACHE = getDebugFlag(
             "ENABLE_DEEP_SHORTCUT_ICON_CACHE", true, "R/W deep shortcut in IconCache");
@@ -137,7 +136,7 @@
             "ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER", true, "Show launcher preview in grid picker");
 
     public static final BooleanFlag USE_SURFACE_VIEW_FOR_GRID_PREVIEW = getDebugFlag(
-            "USE_SURFACE_VIEW_FOR_GRID_PREVIEW", false, "Use surface view for grid preview");
+            "USE_SURFACE_VIEW_FOR_GRID_PREVIEW", true, "Use surface view for grid preview");
 
     public static final BooleanFlag ENABLE_OVERVIEW_ACTIONS = getDebugFlag(
             "ENABLE_OVERVIEW_ACTIONS", true, "Show app actions instead of the shelf in Overview."
diff --git a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
index 75693c6..9b91a1d 100644
--- a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
@@ -26,7 +26,6 @@
 import android.graphics.Rect;
 import android.os.Handler;
 import android.os.Looper;
-import android.os.SystemClock;
 import android.util.Log;
 import android.view.DragEvent;
 import android.view.View;
@@ -63,7 +62,6 @@
 
     protected Launcher mLauncher;
     private DragController mDragController;
-    private long mDragStartTime;
 
     public BaseItemDragListener(Rect previewRect, int previewBitmapWidth, int previewViewWidth) {
         mPreviewRect = previewRect;
@@ -102,7 +100,7 @@
                 return false;
             }
         }
-        return mDragController.onDragEvent(mDragStartTime, event);
+        return mDragController.onDragEvent(event);
     }
 
     protected boolean onDragStart(DragEvent event) {
@@ -118,7 +116,7 @@
 
         Point downPos = new Point((int) event.getX(), (int) event.getY());
         DragOptions options = new DragOptions();
-        options.systemDndStartPoint = downPos;
+        options.simulatedDndStartPoint = downPos;
         options.preDragCondition = preDragCondition;
 
         // We use drag event position as the screenPos for the preview image. Since mPreviewRect
@@ -128,7 +126,6 @@
         // to source window.
         createDragHelper().startDrag(new Rect(mPreviewRect),
                 mPreviewBitmapWidth, mPreviewViewWidth, downPos, this, options);
-        mDragStartTime = SystemClock.uptimeMillis();
         return true;
     }
 
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index ffaada1..d0d9aaf 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -43,7 +43,6 @@
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
 import com.android.launcher3.util.ItemInfoMatcher;
-import com.android.launcher3.util.Thunk;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.util.UiThreadHelper;
 
@@ -61,8 +60,8 @@
      */
     private static final int DEEP_PRESS_DISTANCE_FACTOR = 3;
 
-    @Thunk Launcher mLauncher;
-    private FlingToDeleteHelper mFlingToDeleteHelper;
+    private final Launcher mLauncher;
+    private final FlingToDeleteHelper mFlingToDeleteHelper;
 
     // temporaries to avoid gc thrash
     private Rect mRectTemp = new Rect();
@@ -77,11 +76,12 @@
     /** Options controlling the drag behavior. */
     private DragOptions mOptions;
 
-    /** X coordinate of the down event. */
-    private int mMotionDownX;
+    /** Coordinate for motion down event */
+    private final Point mMotionDown = new Point();
+    /** Coordinate for last touch event **/
+    private final Point mLastTouch = new Point();
 
-    /** Y coordinate of the down event. */
-    private int mMotionDownY;
+    private final Point mTmpPoint = new Point();
 
     private DropTarget.DragObject mDragObject;
 
@@ -96,12 +96,9 @@
 
     private DropTarget mLastDropTarget;
 
-    private final int[] mLastTouch = new int[2];
-    private long mLastTouchUpTime = -1;
     private int mLastTouchClassification;
     private int mDistanceSinceScroll = 0;
 
-    private int mTmpPoint[] = new int[2];
     private Rect mDragLayerRect = new Rect();
 
     private boolean mIsInPreDrag;
@@ -161,13 +158,13 @@
         AbstractFloatingView.closeOpenViews(mLauncher, false, TYPE_DISCOVERY_BOUNCE);
 
         mOptions = options;
-        if (mOptions.systemDndStartPoint != null) {
-            mLastTouch[0] = mMotionDownX = mOptions.systemDndStartPoint.x;
-            mLastTouch[1] = mMotionDownY = mOptions.systemDndStartPoint.y;
+        if (mOptions.simulatedDndStartPoint != null) {
+            mLastTouch.x = mMotionDown.x = mOptions.simulatedDndStartPoint.x;
+            mLastTouch.y = mMotionDown.y = mOptions.simulatedDndStartPoint.y;
         }
 
-        final int registrationX = mMotionDownX - dragLayerX;
-        final int registrationY = mMotionDownY - dragLayerY;
+        final int registrationX = mMotionDown.x - dragLayerX;
+        final int registrationY = mMotionDown.y - dragLayerY;
 
         final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
         final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;
@@ -187,17 +184,13 @@
                 registrationY, initialDragViewScale, dragViewScaleOnDrop, scaleDps);
         dragView.setItemInfo(dragInfo);
         mDragObject.dragComplete = false;
-        if (mOptions.isAccessibleDrag) {
-            // For an accessible drag, we assume the view is being dragged from the center.
-            mDragObject.xOffset = b.getWidth() / 2;
-            mDragObject.yOffset = b.getHeight() / 2;
-            mDragObject.accessibleDrag = true;
-        } else {
-            mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);
-            mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);
-            mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);
 
-            mDragDriver = DragDriver.create(mLauncher, this, mDragObject, mOptions);
+        mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft);
+        mDragObject.yOffset = mMotionDown.y - (dragLayerY + dragRegionTop);
+
+        mDragDriver = DragDriver.create(this, mOptions, mFlingToDeleteHelper::recordMotionEvent);
+        if (!mOptions.isAccessibleDrag) {
+            mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);
         }
 
         mDragObject.dragSource = source;
@@ -213,7 +206,7 @@
         }
 
         mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
-        dragView.show(mLastTouch[0], mLastTouch[1]);
+        dragView.show(mLastTouch.x, mLastTouch.y);
         mDistanceSinceScroll = 0;
 
         if (!mIsInPreDrag) {
@@ -222,7 +215,7 @@
             mOptions.preDragCondition.onPreDragStart(mDragObject);
         }
 
-        handleMoveEvent(mLastTouch[0], mLastTouch[1]);
+        handleMoveEvent(mLastTouch.x, mLastTouch.y);
         mLauncher.getUserEventDispatcher().resetActionDurationMillis();
         return dragView;
     }
@@ -339,7 +332,7 @@
                 }
             }
         };
-        mDragObject.dragView.animateTo(mMotionDownX, mMotionDownY, onCompleteRunnable, duration);
+        mDragObject.dragView.animateTo(mMotionDown.x, mMotionDown.y, onCompleteRunnable, duration);
     }
 
     private void callOnDragEnd() {
@@ -368,30 +361,17 @@
     /**
      * Clamps the position to the drag layer bounds.
      */
-    private int[] getClampedDragLayerPos(float x, float y) {
+    private Point getClampedDragLayerPos(float x, float y) {
         mLauncher.getDragLayer().getLocalVisibleRect(mDragLayerRect);
-        mTmpPoint[0] = (int) Math.max(mDragLayerRect.left, Math.min(x, mDragLayerRect.right - 1));
-        mTmpPoint[1] = (int) Math.max(mDragLayerRect.top, Math.min(y, mDragLayerRect.bottom - 1));
+        mTmpPoint.x = (int) Math.max(mDragLayerRect.left, Math.min(x, mDragLayerRect.right - 1));
+        mTmpPoint.y = (int) Math.max(mDragLayerRect.top, Math.min(y, mDragLayerRect.bottom - 1));
         return mTmpPoint;
     }
 
-    public long getLastGestureUpTime() {
-        if (mDragDriver != null) {
-            return System.currentTimeMillis();
-        } else {
-            return mLastTouchUpTime;
-        }
-    }
-
-    public void resetLastGestureUpTime() {
-        mLastTouchUpTime = -1;
-    }
-
     @Override
     public void onDriverDragMove(float x, float y) {
-        final int[] dragLayerPos = getClampedDragLayerPos(x, y);
-
-        handleMoveEvent(dragLayerPos[0], dragLayerPos[1]);
+        Point dragLayerPos = getClampedDragLayerPos(x, y);
+        handleMoveEvent(dragLayerPos.x, dragLayerPos.y);
     }
 
     @Override
@@ -425,53 +405,38 @@
     /**
      * Call this from a drag source view.
      */
+    @Override
     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
         if (mOptions != null && mOptions.isAccessibleDrag) {
             return false;
         }
 
-        // Update the velocity tracker
-        mFlingToDeleteHelper.recordMotionEvent(ev);
+        Point dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
+        mLastTouch.set(dragLayerPos.x,  dragLayerPos.y);
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            // Remember location of down touch
+            mMotionDown.set(dragLayerPos.x,  dragLayerPos.y);
+        }
 
-        final int action = ev.getAction();
-        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
-        final int dragLayerX = dragLayerPos[0];
-        final int dragLayerY = dragLayerPos[1];
-        mLastTouch[0] = dragLayerX;
-        mLastTouch[1] = dragLayerY;
         if (ATLEAST_Q) {
             mLastTouchClassification = ev.getClassification();
         }
-
-        switch (action) {
-            case MotionEvent.ACTION_DOWN:
-                // Remember location of down touch
-                mMotionDownX = dragLayerX;
-                mMotionDownY = dragLayerY;
-                break;
-            case MotionEvent.ACTION_UP:
-                mLastTouchUpTime = System.currentTimeMillis();
-                break;
-        }
-
         return mDragDriver != null && mDragDriver.onInterceptTouchEvent(ev);
     }
 
     /**
      * Call this from a drag source view.
      */
-    public boolean onDragEvent(long dragStartTime, DragEvent event) {
-        mFlingToDeleteHelper.recordDragEvent(dragStartTime, event);
-        return mDragDriver != null && mDragDriver.onDragEvent(event);
+    @Override
+    public boolean onControllerTouchEvent(MotionEvent ev) {
+        return mDragDriver != null && mDragDriver.onTouchEvent(ev);
     }
 
     /**
-     * Call this from a drag view.
+     * Call this from a drag source view.
      */
-    public void onDragViewAnimationEnd() {
-        if (mDragDriver != null) {
-            mDragDriver.onDragViewAnimationEnd();
-        }
+    public boolean onDragEvent(DragEvent event) {
+        return mDragDriver != null && mDragDriver.onDragEvent(event);
     }
 
     /**
@@ -496,9 +461,8 @@
         checkTouchMove(dropTarget);
 
         // Check if we are hovering over the scroll areas
-        mDistanceSinceScroll += Math.hypot(mLastTouch[0] - x, mLastTouch[1] - y);
-        mLastTouch[0] = x;
-        mLastTouch[1] = y;
+        mDistanceSinceScroll += Math.hypot(mLastTouch.x - x, mLastTouch.y - y);
+        mLastTouch.set(x, y);
 
         int distanceDragged = mDistanceSinceScroll;
         if (ATLEAST_Q && mLastTouchClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS) {
@@ -516,7 +480,7 @@
 
     public void forceTouchMove() {
         int[] dummyCoordinates = mCoordinatesTemp;
-        DropTarget dropTarget = findDropTarget(mLastTouch[0], mLastTouch[1], dummyCoordinates);
+        DropTarget dropTarget = findDropTarget(mLastTouch.x, mLastTouch.y, dummyCoordinates);
         mDragObject.x = dummyCoordinates[0];
         mDragObject.y = dummyCoordinates[1];
         checkTouchMove(dropTarget);
@@ -540,44 +504,6 @@
     }
 
     /**
-     * Call this from a drag source view.
-     */
-    public boolean onControllerTouchEvent(MotionEvent ev) {
-        if (mDragDriver == null || mOptions == null || mOptions.isAccessibleDrag) {
-            return false;
-        }
-
-        // Update the velocity tracker
-        mFlingToDeleteHelper.recordMotionEvent(ev);
-
-        final int action = ev.getAction();
-        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
-        final int dragLayerX = dragLayerPos[0];
-        final int dragLayerY = dragLayerPos[1];
-
-        switch (action) {
-            case MotionEvent.ACTION_DOWN:
-                // Remember where the motion event started
-                mMotionDownX = dragLayerX;
-                mMotionDownY = dragLayerY;
-                break;
-        }
-
-        return mDragDriver.onTouchEvent(ev);
-    }
-
-    /**
-     * Since accessible drag and drop won't cause the same sequence of touch events, we manually
-     * inject the appropriate state which would have been otherwise initiated via touch events.
-     */
-    public void prepareAccessibleDrag(int x, int y) {
-        mMotionDownX = x;
-        mMotionDownY = y;
-        mLastTouch[0] = x;
-        mLastTouch[1] = y;
-    }
-
-    /**
      * As above, since accessible drag and drop won't cause the same sequence of touch events,
      * we manually ensure appropriate drag and drop events get emulated for accessible drag.
      */
diff --git a/src/com/android/launcher3/dragndrop/DragDriver.java b/src/com/android/launcher3/dragndrop/DragDriver.java
index 87461d5..d4ce308 100644
--- a/src/com/android/launcher3/dragndrop/DragDriver.java
+++ b/src/com/android/launcher3/dragndrop/DragDriver.java
@@ -16,19 +16,19 @@
 
 package com.android.launcher3.dragndrop;
 
-import android.content.Context;
-import android.util.Log;
+import android.os.SystemClock;
 import android.view.DragEvent;
 import android.view.MotionEvent;
 
-import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.testing.TestProtocol;
+import java.util.function.Consumer;
 
 /**
  * Base class for driving a drag/drop operation.
  */
 public abstract class DragDriver {
+
     protected final EventListener mEventListener;
+    protected final Consumer<MotionEvent> mSecondaryEventConsumer;
 
     public interface EventListener {
         void onDriverDragMove(float x, float y);
@@ -37,131 +37,175 @@
         void onDriverDragCancel();
     }
 
-    public DragDriver(EventListener eventListener) {
+    public DragDriver(EventListener eventListener, Consumer<MotionEvent> sec) {
         mEventListener = eventListener;
+        mSecondaryEventConsumer = sec;
     }
 
     /**
-     * Handles ending of the DragView animation.
+     * Called to handle system touch event
      */
-    public void onDragViewAnimationEnd() { }
-
     public boolean onTouchEvent(MotionEvent ev) {
-        final int action = ev.getAction();
-
-        switch (action) {
-            case MotionEvent.ACTION_MOVE:
-                mEventListener.onDriverDragMove(ev.getX(), ev.getY());
-                break;
-            case MotionEvent.ACTION_UP:
-                mEventListener.onDriverDragMove(ev.getX(), ev.getY());
-                mEventListener.onDriverDragEnd(ev.getX(), ev.getY());
-                break;
-            case MotionEvent.ACTION_CANCEL:
-                mEventListener.onDriverDragCancel();
-                break;
-        }
-
-        return true;
+        return false;
     }
 
-    public abstract boolean onDragEvent (DragEvent event);
-
-
+    /**
+     * Called to handle system touch intercept event
+     */
     public boolean onInterceptTouchEvent(MotionEvent ev) {
-        final int action = ev.getAction();
-
-        switch (action) {
-            case MotionEvent.ACTION_UP:
-                mEventListener.onDriverDragEnd(ev.getX(), ev.getY());
-                break;
-            case MotionEvent.ACTION_CANCEL:
-                mEventListener.onDriverDragCancel();
-                break;
-        }
-
-        return true;
+        return false;
     }
 
-    public static DragDriver create(Context context, DragController dragController,
-            DragObject dragObject, DragOptions options) {
-        if (options.systemDndStartPoint != null) {
-            return new SystemDragDriver(dragController, context, dragObject);
+    /**
+     * Called to handle system drag event
+     */
+    public boolean onDragEvent(DragEvent event) {
+        return false;
+    }
+
+    /**
+     * Created a driver for handing the actual events
+     */
+    public static DragDriver create(DragController dragController, DragOptions options,
+            Consumer<MotionEvent> sec) {
+        if (options.simulatedDndStartPoint != null) {
+            if  (options.isAccessibleDrag) {
+                return null;
+            }
+            return new SystemDragDriver(dragController, sec);
         } else {
-            return new InternalDragDriver(dragController);
+            return new InternalDragDriver(dragController, sec);
+        }
+    }
+
+    /**
+     * Class for driving a system (i.e. framework) drag/drop operation.
+     */
+    static class SystemDragDriver extends DragDriver {
+
+        private final long mDragStartTime;
+        float mLastX = 0;
+        float mLastY = 0;
+
+        SystemDragDriver(DragController dragController, Consumer<MotionEvent> sec) {
+            super(dragController, sec);
+            mDragStartTime = SystemClock.uptimeMillis();
+        }
+
+        @Override
+        public boolean onInterceptTouchEvent(MotionEvent ev) {
+            return false;
+        }
+
+        /**
+         * It creates a temporary {@link MotionEvent} object for secondary consumer
+         */
+        private void simulateSecondaryMotionEvent(DragEvent event) {
+            final int motionAction;
+            switch (event.getAction()) {
+                case DragEvent.ACTION_DRAG_STARTED:
+                    motionAction = MotionEvent.ACTION_DOWN;
+                    break;
+                case DragEvent.ACTION_DRAG_LOCATION:
+                    motionAction = MotionEvent.ACTION_MOVE;
+                    break;
+                case DragEvent.ACTION_DRAG_ENDED:
+                    motionAction = MotionEvent.ACTION_UP;
+                    break;
+                default:
+                    return;
+            }
+            MotionEvent emulatedEvent = MotionEvent.obtain(mDragStartTime,
+                    SystemClock.uptimeMillis(), motionAction, event.getX(), event.getY(), 0);
+            mSecondaryEventConsumer.accept(emulatedEvent);
+            emulatedEvent.recycle();
+        }
+
+        @Override
+        public boolean onDragEvent(DragEvent event) {
+            simulateSecondaryMotionEvent(event);
+            final int action = event.getAction();
+
+            switch (action) {
+                case DragEvent.ACTION_DRAG_STARTED:
+                    mLastX = event.getX();
+                    mLastY = event.getY();
+                    return true;
+
+                case DragEvent.ACTION_DRAG_ENTERED:
+                    return true;
+
+                case DragEvent.ACTION_DRAG_LOCATION:
+                    mLastX = event.getX();
+                    mLastY = event.getY();
+                    mEventListener.onDriverDragMove(event.getX(), event.getY());
+                    return true;
+
+                case DragEvent.ACTION_DROP:
+                    mLastX = event.getX();
+                    mLastY = event.getY();
+                    mEventListener.onDriverDragMove(event.getX(), event.getY());
+                    mEventListener.onDriverDragEnd(mLastX, mLastY);
+                    return true;
+                case DragEvent.ACTION_DRAG_EXITED:
+                    mEventListener.onDriverDragExitWindow();
+                    return true;
+
+                case DragEvent.ACTION_DRAG_ENDED:
+                    mEventListener.onDriverDragCancel();
+                    return true;
+
+                default:
+                    return false;
+            }
+        }
+    }
+
+    /**
+     * Class for driving an internal (i.e. not using framework) drag/drop operation.
+     */
+    static class InternalDragDriver extends DragDriver {
+        InternalDragDriver(DragController dragController, Consumer<MotionEvent> sec) {
+            super(dragController, sec);
+        }
+
+        @Override
+        public boolean onTouchEvent(MotionEvent ev) {
+            mSecondaryEventConsumer.accept(ev);
+            final int action = ev.getAction();
+
+            switch (action) {
+                case MotionEvent.ACTION_MOVE:
+                    mEventListener.onDriverDragMove(ev.getX(), ev.getY());
+                    break;
+                case MotionEvent.ACTION_UP:
+                    mEventListener.onDriverDragMove(ev.getX(), ev.getY());
+                    mEventListener.onDriverDragEnd(ev.getX(), ev.getY());
+                    break;
+                case MotionEvent.ACTION_CANCEL:
+                    mEventListener.onDriverDragCancel();
+                    break;
+            }
+
+            return true;
+        }
+
+
+        public boolean onInterceptTouchEvent(MotionEvent ev) {
+            mSecondaryEventConsumer.accept(ev);
+            final int action = ev.getAction();
+
+            switch (action) {
+                case MotionEvent.ACTION_UP:
+                    mEventListener.onDriverDragEnd(ev.getX(), ev.getY());
+                    break;
+                case MotionEvent.ACTION_CANCEL:
+                    mEventListener.onDriverDragCancel();
+                    break;
+            }
+            return true;
         }
     }
 }
 
-/**
- * Class for driving a system (i.e. framework) drag/drop operation.
- */
-class SystemDragDriver extends DragDriver {
 
-    float mLastX = 0;
-    float mLastY = 0;
-
-    SystemDragDriver(DragController dragController, Context context, DragObject dragObject) {
-        super(dragController);
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent ev) {
-        return false;
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        return false;
-    }
-
-    @Override
-    public boolean onDragEvent (DragEvent event) {
-        final int action = event.getAction();
-
-        switch (action) {
-            case DragEvent.ACTION_DRAG_STARTED:
-                mLastX = event.getX();
-                mLastY = event.getY();
-                return true;
-
-            case DragEvent.ACTION_DRAG_ENTERED:
-                return true;
-
-            case DragEvent.ACTION_DRAG_LOCATION:
-                mLastX = event.getX();
-                mLastY = event.getY();
-                mEventListener.onDriverDragMove(event.getX(), event.getY());
-                return true;
-
-            case DragEvent.ACTION_DROP:
-                mLastX = event.getX();
-                mLastY = event.getY();
-                mEventListener.onDriverDragMove(event.getX(), event.getY());
-                mEventListener.onDriverDragEnd(mLastX, mLastY);
-                return true;
-            case DragEvent.ACTION_DRAG_EXITED:
-                mEventListener.onDriverDragExitWindow();
-                return true;
-
-            case DragEvent.ACTION_DRAG_ENDED:
-                mEventListener.onDriverDragCancel();
-                return true;
-
-            default:
-                return false;
-        }
-    }
-}
-
-/**
- * Class for driving an internal (i.e. not using framework) drag/drop operation.
- */
-class InternalDragDriver extends DragDriver {
-    InternalDragDriver(DragController dragController) {
-        super(dragController);
-    }
-
-    @Override
-    public boolean onDragEvent (DragEvent event) { return false; }
-}
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 409a1d5..e18ca54 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -546,7 +546,7 @@
     @Override
     public void setInsets(Rect insets) {
         super.setInsets(insets);
-        mWorkspaceScrim.onInsetsChanged(insets);
+        mWorkspaceScrim.onInsetsChanged(insets, mAllowSysuiScrims);
         mOverviewScrim.onInsetsChanged(insets);
     }
 
diff --git a/src/com/android/launcher3/dragndrop/DragOptions.java b/src/com/android/launcher3/dragndrop/DragOptions.java
index 2d19f36..959602b 100644
--- a/src/com/android/launcher3/dragndrop/DragOptions.java
+++ b/src/com/android/launcher3/dragndrop/DragOptions.java
@@ -28,8 +28,11 @@
     /** Whether or not an accessible drag operation is in progress. */
     public boolean isAccessibleDrag = false;
 
-    /** Specifies the start location for the system DnD, null when using internal DnD */
-    public Point systemDndStartPoint = null;
+    /**
+     * Specifies the start location for a simulated DnD (like system drag or accessibility drag),
+     * null when using internal DnD
+     */
+    public Point simulatedDndStartPoint = null;
 
     /** Determines when a pre-drag should transition to a drag. By default, this is immediate. */
     public PreDragCondition preDragCondition = null;
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 145885a..7c76d34 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -19,8 +19,6 @@
 import static com.android.launcher3.Utilities.getBadge;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
 import android.animation.FloatArrayEvaluator;
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
@@ -146,15 +144,6 @@
             }
         });
 
-        mAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                if (!mAnimationCancelled) {
-                    mDragController.onDragViewAnimationEnd();
-                }
-            }
-        });
-
         mBitmap = bitmap;
         setDragRegion(new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()));
 
diff --git a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
index 06b5c40..7788f93 100644
--- a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
+++ b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
@@ -17,8 +17,6 @@
 package com.android.launcher3.dragndrop;
 
 import android.graphics.PointF;
-import android.os.SystemClock;
-import android.view.DragEvent;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
@@ -55,31 +53,6 @@
         mVelocityTracker.addMovement(ev);
     }
 
-    /**
-     * Same as {@link #recordMotionEvent}. It creates a temporary {@link MotionEvent} object
-     * using {@param event} for tracking velocity.
-     */
-    public void recordDragEvent(long dragStartTime, DragEvent event) {
-        final int motionAction;
-        switch (event.getAction()) {
-            case DragEvent.ACTION_DRAG_STARTED:
-                motionAction = MotionEvent.ACTION_DOWN;
-                break;
-            case DragEvent.ACTION_DRAG_LOCATION:
-                motionAction = MotionEvent.ACTION_MOVE;
-                break;
-            case DragEvent.ACTION_DRAG_ENDED:
-                motionAction = MotionEvent.ACTION_UP;
-                break;
-            default:
-                return;
-        }
-        MotionEvent emulatedEvent = MotionEvent.obtain(dragStartTime, SystemClock.uptimeMillis(),
-                motionAction, event.getX(), event.getY(), 0);
-        recordMotionEvent(emulatedEvent);
-        emulatedEvent.recycle();
-    }
-
     public void releaseVelocityTracker() {
         if (mVelocityTracker != null) {
             mVelocityTracker.recycle();
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 5f496f4..14fa1f4 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -85,6 +85,7 @@
 import com.android.launcher3.Workspace.ItemOperator;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
+import com.android.launcher3.accessibility.FolderAccessibilityHelper;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragController.DragListener;
@@ -271,16 +272,15 @@
             mDragController.addDragListener(this);
             if (options.isAccessibleDrag) {
                 mDragController.addDragListener(new AccessibleDragListenerAdapter(
-                        mContent, CellLayout.FOLDER_ACCESSIBILITY_DRAG) {
-
-                    @Override
-                    protected void enableAccessibleDrag(boolean enable) {
-                        super.enableAccessibleDrag(enable);
-                        mFooter.setImportantForAccessibility(enable
-                                ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                                : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-                    }
-                });
+                        mContent, FolderAccessibilityHelper::new) {
+                            @Override
+                            protected void enableAccessibleDrag(boolean enable) {
+                                super.enableAccessibleDrag(enable);
+                                mFooter.setImportantForAccessibility(enable
+                                        ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                                        : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+                            }
+                        });
             }
 
             mLauncher.getWorkspace().beginDragShared(v, this, options);
@@ -519,9 +519,6 @@
     }
 
     private void startAnimation(final AnimatorSet a) {
-        if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
-            mCurrentAnimator.cancel();
-        }
         final Workspace workspace = mLauncher.getWorkspace();
         final CellLayout currentCellLayout =
                 (CellLayout) workspace.getChildAt(workspace.getCurrentPage());
@@ -638,6 +635,9 @@
         // dropping. One resulting issue is that replaceFolderWithFinalItem() can be called twice.
         mDeleteFolderOnDropCompleted = false;
 
+        if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
+            mCurrentAnimator.cancel();
+        }
         AnimatorSet anim = new FolderAnimationManager(this, true /* isOpening */).getAnimator();
         anim.addListener(new AnimatorListenerAdapter() {
             @Override
@@ -741,6 +741,9 @@
     }
 
     private void animateClosed() {
+        if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
+            mCurrentAnimator.cancel();
+        }
         AnimatorSet a = new FolderAnimationManager(this, false /* isOpening */).getAnimator();
         a.addListener(new AnimatorListenerAdapter() {
             @Override
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index f72e674..b83609e 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
 import static com.android.launcher3.graphics.IconShape.getShape;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
-import static com.android.launcher3.uioverrides.BackgroundBlurController.BACKGROUND_BLUR;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -47,7 +46,6 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PropertyResetListener;
 import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.uioverrides.BackgroundBlurController;
 import com.android.launcher3.util.Themes;
 
 import java.util.List;
@@ -222,14 +220,6 @@
         Animator z = getAnimator(mFolder, View.TRANSLATION_Z, -mFolder.getElevation(), 0);
         play(a, z, mIsOpening ? midDuration : 0, midDuration);
 
-        BackgroundBlurController blurController = mLauncher.getBackgroundBlurController();
-        int stateBackgroundBlur = mLauncher.getStateManager().getState()
-                .getBackgroundBlurRadius(mLauncher);
-        int folderBackgroundBlurAdjustment = blurController.getFolderBackgroundBlurAdjustment();
-        play(a, ObjectAnimator.ofInt(blurController, BACKGROUND_BLUR, mIsOpening
-                ? stateBackgroundBlur + folderBackgroundBlurAdjustment
-                : stateBackgroundBlur));
-
         // Store clip variables
         CellLayout cellLayout = mContent.getCurrentCellLayout();
         boolean folderClipChildren = mFolder.getClipChildren();
diff --git a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java b/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
index 2c7f891..3fb2bf6 100644
--- a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
+++ b/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
@@ -194,10 +194,13 @@
         return anim;
     }
 
-    public void onInsetsChanged(Rect insets) {
-        mDrawTopScrim = mTopScrim != null && insets.top > 0;
-        mDrawBottomScrim = mBottomMask != null &&
-                !mLauncher.getDeviceProfile().isVerticalBarLayout();
+    /**
+     * Determines whether to draw the top and/or bottom scrim based on new insets.
+     */
+    public void onInsetsChanged(Rect insets, boolean allowSysuiScrims) {
+        mDrawTopScrim = allowSysuiScrims && mTopScrim != null && insets.top > 0;
+        mDrawBottomScrim = allowSysuiScrims && mBottomMask != null
+                && !mLauncher.getDeviceProfile().isVerticalBarLayout();
     }
 
     @Override
@@ -236,14 +239,6 @@
         }
     }
 
-    public void hideSysUiScrim(boolean hideSysUiScrim) {
-        mHideSysUiScrim = hideSysUiScrim || (mTopScrim == null);
-        if (!hideSysUiScrim) {
-            mAnimateScrimOnNextDraw = true;
-        }
-        invalidate();
-    }
-
     private void setSysUiProgress(float progress) {
         if (progress != mSysUiProgress) {
             mSysUiProgress = progress;
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index 04741a1..b0defd4 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -39,7 +39,7 @@
     public static final MainThreadInitializedObject<VibratorWrapper> INSTANCE =
             new MainThreadInitializedObject<>(VibratorWrapper::new);
 
-    private static final VibrationEffect EFFECT_CLICK =
+    public static final VibrationEffect EFFECT_CLICK =
             createPredefined(VibrationEffect.EFFECT_CLICK);
 
     /**
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index 25748ae..868c91d 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -23,7 +23,11 @@
 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 
 import android.annotation.TargetApi;
+import android.app.WallpaperInfo;
+import android.app.WallpaperManager;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.graphics.Insets;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -38,11 +42,15 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.widget.FrameLayout;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.InsettableFrameLayout;
+import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.launcher3.util.TouchController;
 
 import java.io.PrintWriter;
@@ -98,6 +106,10 @@
 
     protected final T mActivity;
     private final MultiValueAlpha mMultiValueAlpha;
+    private final WallpaperManager mWallpaperManager;
+    private final SimpleBroadcastReceiver mWallpaperChangeReceiver =
+            new SimpleBroadcastReceiver(this::onWallpaperChanged);
+    private final String[] mWallpapersWithoutSysuiScrims;
 
     // All the touch controllers for the view
     protected TouchController[] mControllers;
@@ -108,10 +120,15 @@
 
     private TouchCompleteListener mTouchCompleteListener;
 
+    protected boolean mAllowSysuiScrims = true;
+
     public BaseDragLayer(Context context, AttributeSet attrs, int alphaChannelCount) {
         super(context, attrs);
         mActivity = (T) ActivityContext.lookupContext(context);
         mMultiValueAlpha = new MultiValueAlpha(this, alphaChannelCount);
+        mWallpaperManager = context.getSystemService(WallpaperManager.class);
+        mWallpapersWithoutSysuiScrims = getResources().getStringArray(
+                R.array.live_wallpapers_remove_sysui_scrims);
     }
 
     /**
@@ -517,4 +534,46 @@
         }
         return super.dispatchApplyWindowInsets(insets);
     }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mWallpaperChangeReceiver.register(mActivity, Intent.ACTION_WALLPAPER_CHANGED);
+        onWallpaperChanged(null);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mActivity.unregisterReceiver(mWallpaperChangeReceiver);
+    }
+
+    private void onWallpaperChanged(Intent unusedBroadcastIntent) {
+        WallpaperInfo newWallpaperInfo = mWallpaperManager.getWallpaperInfo();
+        boolean oldAllowSysuiScrims = mAllowSysuiScrims;
+        mAllowSysuiScrims = computeAllowSysuiScrims(newWallpaperInfo);
+        if (mAllowSysuiScrims != oldAllowSysuiScrims) {
+            // Reapply insets so scrim can be removed or re-added if necessary.
+            setInsets(mInsets);
+        }
+    }
+
+    /**
+     * Determines whether we can scrim the status bar and nav bar for the given wallpaper by
+     * checking against a list of live wallpapers that we don't show the scrims on.
+     */
+    private boolean computeAllowSysuiScrims(@Nullable WallpaperInfo newWallpaperInfo) {
+        if (newWallpaperInfo == null) {
+            // New wallpaper is static, not live. Thus, blacklist isn't applicable.
+            return true;
+        }
+        ComponentName newWallpaper = newWallpaperInfo.getComponent();
+        for (String wallpaperWithoutScrim : mWallpapersWithoutSysuiScrims) {
+            if (newWallpaper.equals(ComponentName.unflattenFromString(wallpaperWithoutScrim))) {
+                // New wallpaper is blacklisted from showing a scrim.
+                return false;
+            }
+        }
+        return true;
+    }
 }
diff --git a/src/com/android/launcher3/views/WorkFooterContainer.java b/src/com/android/launcher3/views/WorkFooterContainer.java
deleted file mode 100644
index 9ac8230..0000000
--- a/src/com/android/launcher3/views/WorkFooterContainer.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.views;
-
-import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
-
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.launcher3.Insettable;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.allapps.WorkModeSwitch;
-import com.android.launcher3.pm.UserCache;
-
-/**
- * Container to show work footer in all-apps.
- */
-public class WorkFooterContainer extends LinearLayout implements Insettable {
-    private Rect mInsets = new Rect();
-
-    private WorkModeSwitch mWorkModeSwitch;
-    private TextView mWorkModeLabel;
-
-    protected final ObjectAnimator mOpenCloseAnimator;
-
-    public WorkFooterContainer(Context context) {
-        this(context, null, 0);
-    }
-
-    public WorkFooterContainer(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public WorkFooterContainer(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this);
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        super.onLayout(changed, l, t, r, b);
-        updateTranslation();
-        this.setVisibility(shouldShowWorkFooter() ? VISIBLE : GONE);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mWorkModeSwitch = findViewById(R.id.work_mode_toggle);
-        mWorkModeLabel = findViewById(R.id.work_mode_label);
-    }
-
-    @Override
-    public void offsetTopAndBottom(int offset) {
-        super.offsetTopAndBottom(offset);
-        updateTranslation();
-    }
-
-    private void updateTranslation() {
-        if (getParent() instanceof View) {
-            View parent = (View) getParent();
-            int availableBot = parent.getHeight() - parent.getPaddingBottom();
-            setTranslationY(Math.max(0, availableBot - getBottom()));
-        }
-    }
-
-    @Override
-    public void setInsets(Rect insets) {
-        int bottomInset = insets.bottom - mInsets.bottom;
-        mInsets.set(insets);
-        setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
-                getPaddingBottom() + bottomInset);
-    }
-
-    /**
-     * Animates in/out work profile toggle panel based on the tab user is on
-     */
-    public void setWorkTabVisible(boolean workTabVisible) {
-        if (!shouldShowWorkFooter()) return;
-
-        mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat(ALPHA, workTabVisible ? 1 : 0));
-        mOpenCloseAnimator.start();
-    }
-
-    /**
-     * Refreshes views based on current work profile enabled status
-     */
-    public void refresh() {
-        if (!shouldShowWorkFooter()) return;
-        boolean anyProfileQuietModeEnabled = UserCache.INSTANCE.get(
-                getContext()).isAnyProfileQuietModeEnabled();
-
-        mWorkModeLabel.setText(anyProfileQuietModeEnabled
-                ? R.string.work_mode_off_label : R.string.work_mode_on_label);
-        mWorkModeLabel.setCompoundDrawablesWithIntrinsicBounds(
-                anyProfileQuietModeEnabled ? R.drawable.ic_corp_off : R.drawable.ic_corp, 0, 0, 0);
-        mWorkModeSwitch.refresh();
-    }
-
-    /**
-     * Returns work mode switch
-     */
-    public WorkModeSwitch getWorkModeSwitch() {
-        return mWorkModeSwitch;
-    }
-
-    private boolean shouldShowWorkFooter() {
-        Launcher launcher = Launcher.getLauncher(getContext());
-        return Utilities.ATLEAST_P && (hasShortcutsPermission(launcher)
-                || launcher.checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
-                == PackageManager.PERMISSION_GRANTED);
-    }
-}
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/BackgroundBlurController.java b/src_ui_overrides/com/android/launcher3/uioverrides/DepthController.java
similarity index 66%
rename from src_ui_overrides/com/android/launcher3/uioverrides/BackgroundBlurController.java
rename to src_ui_overrides/com/android/launcher3/uioverrides/DepthController.java
index 75f99a9..7ad85e2 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/BackgroundBlurController.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/DepthController.java
@@ -17,7 +17,7 @@
 package com.android.launcher3.uioverrides;
 
 
-import android.util.IntProperty;
+import android.util.FloatProperty;
 import android.view.View;
 
 import com.android.launcher3.Launcher;
@@ -27,26 +27,22 @@
 import com.android.launcher3.states.StateAnimationConfig;
 
 /**
- * Controls the blur, for the Launcher surface only.
+ * Controls blur and wallpaper zoom, for the Launcher surface only.
  */
-public class BackgroundBlurController implements LauncherStateManager.StateHandler {
+public class DepthController implements LauncherStateManager.StateHandler {
 
-    public static final IntProperty<BackgroundBlurController> BACKGROUND_BLUR =
-            new IntProperty<BackgroundBlurController>("backgroundBlur") {
+    public static final FloatProperty<DepthController> DEPTH =
+            new FloatProperty<DepthController>("depth") {
                 @Override
-                public void setValue(BackgroundBlurController blurController, int blurRadius) {}
+                public void setValue(DepthController depthController, float depth) {}
 
                 @Override
-                public Integer get(BackgroundBlurController blurController) {
-                    return 0;
+                public Float get(DepthController depthController) {
+                    return 0f;
                 }
             };
 
-    public BackgroundBlurController(Launcher l) {}
-
-    public int getFolderBackgroundBlurAdjustment() {
-        return 0;
-    }
+    public DepthController(Launcher l) {}
 
     public void setSurfaceToLauncher(View v) {}
 
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 3d12248..551b533 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -102,6 +102,7 @@
 
     private static String sDetectedActivityLeak;
     private static boolean sActivityLeakReported;
+    private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
 
     protected LooperExecutor mMainThreadExecutor = MAIN_EXECUTOR;
     protected final UiDevice mDevice = UiDevice.getInstance(getInstrumentation());
@@ -213,8 +214,21 @@
         return mDevice;
     }
 
+    private boolean hasSystemUiObject(String resId) {
+        return mDevice.hasObject(By.res(SYSTEMUI_PACKAGE, resId));
+    }
+
     @Before
     public void setUp() throws Exception {
+        mDevice.executeShellCommand("setprop vendor.battery.defender.disable 1");
+        mDevice.executeShellCommand("settings put global stay_on_while_plugged_in 3");
+        if (hasSystemUiObject("keyguard_status_view")) {
+            Log.d(TAG, "Before unlocking the phone");
+            mDevice.executeShellCommand("input keyevent 82");
+        } else {
+            Log.d(TAG, "Phone isn't locked");
+        }
+
         final String launcherPackageName = mDevice.getLauncherPackageName();
         try {
             final Context context = InstrumentationRegistry.getContext();
diff --git a/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src/com/android/launcher3/ui/WorkTabTest.java
index db2d974..7e80e5d 100644
--- a/tests/src/com/android/launcher3/ui/WorkTabTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkTabTest.java
@@ -31,9 +31,9 @@
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsPagedView;
+import com.android.launcher3.allapps.WorkModeSwitch;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.views.WorkEduView;
-import com.android.launcher3.views.WorkFooterContainer;
 
 import org.junit.After;
 import org.junit.Before;
@@ -87,7 +87,7 @@
         executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
         waitForState("Launcher internal state didn't switch to All Apps", () -> ALL_APPS);
         getOnceNotNull("Apps view did not bind",
-                launcher -> launcher.getAppsView().getWorkFooterContainer(), 60000);
+                launcher -> launcher.getAppsView().getWorkModeSwitch(), 60000);
 
         UserManager userManager = getFromLauncher(l -> l.getSystemService(UserManager.class));
         assertEquals(2, userManager.getUserProfiles().size());
@@ -102,10 +102,10 @@
 
         assertTrue(userManager.isQuietModeEnabled(workProfile));
         executeOnLauncher(launcher -> {
-            WorkFooterContainer wf = launcher.getAppsView().getWorkFooterContainer();
+            WorkModeSwitch wf = launcher.getAppsView().getWorkModeSwitch();
             ((AllAppsPagedView) launcher.getAppsView().getContentView()).snapToPageImmediately(
                     AllAppsContainerView.AdapterHolder.WORK);
-            wf.getWorkModeSwitch().toggle();
+            wf.toggle();
         });
         waitForLauncherCondition("Work toggle did not work",
                 l -> l.getSystemService(UserManager.class).isQuietModeEnabled(workProfile));