Merge "Import translations. DO NOT MERGE" into ub-launcher3-rvc-dev
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
index fdb8e4c..d200868 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/ComponentKeyMapper.java
@@ -21,7 +21,6 @@
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
-import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ComponentKey;
 
 public class ComponentKeyMapper {
@@ -57,9 +56,8 @@
             return item;
         } else if (getComponentClass().equals(COMPONENT_CLASS_MARKER)) {
             return mCache.getInstantApp(componentKey.componentName.getPackageName());
-        } else if (componentKey instanceof ShortcutKey) {
-            return mCache.getShortcutInfo((ShortcutKey) componentKey);
+        } else {
+            return mCache.getShortcutInfo(componentKey);
         }
-        return null;
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
index 6c4bfe8..ab96b1340 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
@@ -45,6 +45,7 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.InstantAppResolver;
 
 import java.util.ArrayList;
@@ -76,7 +77,7 @@
     private final Runnable mOnUpdateCallback;
     private final IconCache mIconCache;
 
-    private final Map<ShortcutKey, WorkspaceItemInfo> mShortcuts;
+    private final Map<ComponentKey, WorkspaceItemInfo> mShortcuts;
     private final Map<String, InstantAppItemInfo> mInstantApps;
 
     public DynamicItemCache(Context context, Runnable onUpdateCallback) {
@@ -230,7 +231,7 @@
     }
 
     @MainThread
-    public WorkspaceItemInfo getShortcutInfo(ShortcutKey key) {
+    public WorkspaceItemInfo getShortcutInfo(ComponentKey key) {
         return mShortcuts.get(key);
     }
 
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 f907089..78cc2dc 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
@@ -200,6 +200,7 @@
             pageId = LauncherSettings.Settings.call(mLauncher.getContentResolver(),
                     LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
                     .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+            mNewScreens = IntArray.wrap(pageId);
         }
         for (int i = 0; i < mLauncher.getDeviceProfile().inv.numHotseatIcons; i++) {
             View child = mHotseat.getChildAt(i, 0);
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 4213740..fa137f8 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
@@ -33,6 +33,7 @@
 import com.android.launcher3.Insettable;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
+import com.android.launcher3.Workspace;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.UserEventDispatcher;
@@ -153,6 +154,12 @@
     private void logUserAction(boolean migrated, int pageIndex) {
         LauncherLogProto.Action action = new LauncherLogProto.Action();
         LauncherLogProto.Target target = new LauncherLogProto.Target();
+
+        int hotseatItemsCount = mLauncher.getHotseat().getShortcutsAndWidgets().getChildCount();
+        // -1 to exclude smart space
+        int workspaceItemCount = mLauncher.getWorkspace().getScreenWithId(
+                Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets().getChildCount() - 1;
+
         action.type = LauncherLogProto.Action.Type.TOUCH;
         action.touch = LauncherLogProto.Action.Touch.TAP;
         target.containerType = LauncherLogProto.ContainerType.TIP;
@@ -162,7 +169,7 @@
         target.rank = MIGRATION_EXPERIMENT_IDENTIFIER;
         // encoding migration type on pageIndex
         target.pageIndex = pageIndex;
-        target.cardinality = HotseatPredictionController.MAX_ITEMS_FOR_MIGRATION;
+        target.cardinality = (workspaceItemCount * 1000) + hotseatItemsCount;
         LauncherLogProto.LauncherEvent event = newLauncherEvent(action, target);
         UserEventDispatcher.newInstance(getContext()).dispatchUserEvent(event, null);
     }
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 7c00287..1aff8e9 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
@@ -31,6 +31,7 @@
 import android.app.prediction.AppTargetId;
 import android.content.ComponentName;
 import android.os.Bundle;
+import android.os.Process;
 import android.provider.DeviceConfig;
 import android.util.Log;
 import android.view.View;
@@ -40,6 +41,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.CellLayout;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.Hotseat;
@@ -63,6 +65,7 @@
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.shortcuts.ShortcutKey;
@@ -76,6 +79,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 import java.util.OptionalInt;
 import java.util.stream.IntStream;
 
@@ -91,18 +95,19 @@
     private static final String TAG = "PredictiveHotseat";
     private static final boolean DEBUG = false;
 
-    public static final int MAX_ITEMS_FOR_MIGRATION = DeviceConfig.getInt(
-            DeviceFlag.NAMESPACE_LAUNCHER, "max_homepage_items_for_migration", 5);
-
     //TODO: replace this with AppTargetEvent.ACTION_UNPIN (b/144119543)
     private static final int APPTARGET_ACTION_UNPIN = 4;
 
+    private static final String PREDICTED_ITEMS_CACHE_KEY = "predicted_item_keys";
+
     private static final String APP_LOCATION_HOTSEAT = "hotseat";
     private static final String APP_LOCATION_WORKSPACE = "workspace";
 
     private static final String BUNDLE_KEY_HOTSEAT = "hotseat_apps";
     private static final String BUNDLE_KEY_WORKSPACE = "workspace_apps";
 
+    private static final String BUNDLE_KEY_PIN_EVENTS = "pin_events";
+
     private static final String PREDICTION_CLIENT = "hotseat";
     private DropTarget.DragObject mDragObject;
     private int mHotSeatItemsCount;
@@ -119,6 +124,7 @@
     private AllAppsStore mAllAppsStore;
     private AnimatorSet mIconRemoveAnimators;
     private boolean mUIUpdatePaused = false;
+    private boolean mRequiresCacheUpdate = false;
 
     private HotseatEduController mHotseatEduController;
 
@@ -145,6 +151,7 @@
         if (mHotseat.isAttachedToWindow()) {
             onViewAttachedToWindow(mHotseat);
         }
+        showCachedItems();
     }
 
     /**
@@ -294,17 +301,57 @@
         mAppPredictor.requestPredictionUpdate();
     }
 
+    private void showCachedItems() {
+        ArrayList<ComponentKey> componentKeys = getCachedComponentKeys();
+        mComponentKeyMappers.clear();
+        for (ComponentKey key : componentKeys) {
+            mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache));
+        }
+        updateDependencies();
+        fillGapsWithPrediction();
+    }
+
     private Bundle getAppPredictionContextExtra() {
         Bundle bundle = new Bundle();
+
+        //TODO: remove this way of reporting items
         bundle.putParcelableArrayList(BUNDLE_KEY_HOTSEAT,
                 getPinnedAppTargetsInViewGroup((mHotseat.getShortcutsAndWidgets())));
         bundle.putParcelableArrayList(BUNDLE_KEY_WORKSPACE, getPinnedAppTargetsInViewGroup(
                 mLauncher.getWorkspace().getScreenWithId(
                         Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets()));
 
+        ArrayList<AppTargetEvent> pinEvents = new ArrayList<>();
+        getPinEventsForViewGroup(pinEvents, mHotseat.getShortcutsAndWidgets(),
+                APP_LOCATION_HOTSEAT);
+        getPinEventsForViewGroup(pinEvents, mLauncher.getWorkspace().getScreenWithId(
+                Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets(), APP_LOCATION_WORKSPACE);
+        bundle.putParcelableArrayList(BUNDLE_KEY_PIN_EVENTS, pinEvents);
+
         return bundle;
     }
 
+    private ArrayList<AppTargetEvent> getPinEventsForViewGroup(ArrayList<AppTargetEvent> pinEvents,
+            ViewGroup views, String root) {
+        for (int i = 0; i < views.getChildCount(); i++) {
+            View child = views.getChildAt(i);
+            final AppTargetEvent event;
+            if (child.getTag() instanceof ItemInfo && getAppTargetFromInfo(
+                    (ItemInfo) child.getTag()) != null) {
+                ItemInfo info = (ItemInfo) child.getTag();
+                event = wrapAppTargetWithLocation(getAppTargetFromInfo(info),
+                        AppTargetEvent.ACTION_PIN, info);
+            } else {
+                CellLayout.LayoutParams params = (CellLayout.LayoutParams) views.getLayoutParams();
+                event = wrapAppTargetWithLocation(getBlockAppTarget(), AppTargetEvent.ACTION_PIN,
+                        root, 0, params.cellX, params.cellY, params.cellHSpan, params.cellVSpan);
+            }
+            pinEvents.add(event);
+        }
+        return pinEvents;
+    }
+
+
     private ArrayList<AppTarget> getPinnedAppTargetsInViewGroup(ViewGroup viewGroup) {
         ArrayList<AppTarget> pinnedApps = new ArrayList<>();
         for (int i = 0; i < viewGroup.getChildCount(); i++) {
@@ -320,6 +367,7 @@
     private void setPredictedApps(List<AppTarget> appTargets) {
         mComponentKeyMappers.clear();
         StringBuilder predictionLog = new StringBuilder("predictedApps: [\n");
+        ArrayList<ComponentKey> componentKeys = new ArrayList<>();
         for (AppTarget appTarget : appTargets) {
             ComponentKey key;
             if (appTarget.getShortcutInfo() != null) {
@@ -328,6 +376,7 @@
                 key = new ComponentKey(new ComponentName(appTarget.getPackageName(),
                         appTarget.getClassName()), appTarget.getUser());
             }
+            componentKeys.add(key);
             predictionLog.append(key.toString());
             predictionLog.append(",rank:");
             predictionLog.append(appTarget.getRank());
@@ -342,6 +391,35 @@
         } else if (mHotseatEduController != null) {
             mHotseatEduController.setPredictedApps(mapToWorkspaceItemInfo(mComponentKeyMappers));
         }
+        // should invalidate cache if AiAi sends empty list of AppTargets
+        if (appTargets.isEmpty()) {
+            mRequiresCacheUpdate = true;
+        }
+        cachePredictionComponentKeys(componentKeys);
+    }
+
+    private void cachePredictionComponentKeys(ArrayList<ComponentKey> componentKeys) {
+        if (!mRequiresCacheUpdate) return;
+        StringBuilder builder = new StringBuilder();
+        for (ComponentKey componentKey : componentKeys) {
+            builder.append(componentKey);
+            builder.append("\n");
+        }
+        mLauncher.getDevicePrefs().edit().putString(PREDICTED_ITEMS_CACHE_KEY,
+                builder.toString()).apply();
+        mRequiresCacheUpdate = false;
+    }
+
+    private ArrayList<ComponentKey> getCachedComponentKeys() {
+        String cachedBlob = mLauncher.getDevicePrefs().getString(PREDICTED_ITEMS_CACHE_KEY, "");
+        ArrayList<ComponentKey> results = new ArrayList<>();
+        for (String line : cachedBlob.split("\n")) {
+            ComponentKey key = ComponentKey.fromString(line);
+            if (key != null) {
+                results.add(key);
+            }
+        }
+        return results;
     }
 
     private void updateDependencies() {
@@ -367,6 +445,7 @@
         icon.pin(workspaceItemInfo);
         AppTarget appTarget = getAppTargetFromItemInfo(workspaceItemInfo);
         notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, AppTargetEvent.ACTION_PIN);
+        mRequiresCacheUpdate = true;
     }
 
     private List<WorkspaceItemInfo> mapToWorkspaceItemInfo(
@@ -533,6 +612,7 @@
         }
         mDragObject = null;
         fillGapsWithPrediction(true, this::removeOutlineDrawings);
+        mRequiresCacheUpdate = true;
     }
 
     @Nullable
@@ -606,6 +686,9 @@
         if (isReady()) return;
         int hotseatItemsCount = mHotseat.getShortcutsAndWidgets().getChildCount();
 
+        int maxItems = DeviceConfig.getInt(
+                DeviceFlag.NAMESPACE_LAUNCHER, "max_homepage_items_for_migration", 5);
+
         // -1 to exclude smart space
         int workspaceItemCount = mLauncher.getWorkspace().getScreenWithId(
                 Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets().getChildCount() - 1;
@@ -614,7 +697,7 @@
         // open spots in their hotseat and have more than maxItems in their hotseat + workspace
 
         if (hotseatItemsCount == mHotSeatItemsCount && workspaceItemCount + hotseatItemsCount
-                > MAX_ITEMS_FOR_MIGRATION) {
+                > maxItems) {
             mLauncher.getSharedPrefs().edit().putBoolean(HotseatEduController.KEY_HOTSEAT_EDU_SEEN,
                     true).apply();
 
@@ -626,8 +709,8 @@
 
             // temporarily encode details in log target (go/hotseat_migration)
             target.rank = 2;
-            target.cardinality = MAX_ITEMS_FOR_MIGRATION;
-            target.pageIndex = (workspaceItemCount * 1000) + hotseatItemsCount;
+            target.cardinality = (workspaceItemCount * 1000) + hotseatItemsCount;
+            target.pageIndex = maxItems;
             LauncherLogProto.LauncherEvent event = newLauncherEvent(action, target);
             UserEventDispatcher.newInstance(mLauncher).dispatchUserEvent(event, null);
 
@@ -689,4 +772,52 @@
         return new AppTarget.Builder(new AppTargetId("app:" + cn.getPackageName()),
                 cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
     }
+
+    private AppTarget getAppTargetFromInfo(ItemInfo info) {
+        if (info == null) return null;
+        if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+                && info instanceof LauncherAppWidgetInfo
+                && ((LauncherAppWidgetInfo) info).providerName != null) {
+            ComponentName cn = ((LauncherAppWidgetInfo) info).providerName;
+            return new AppTarget.Builder(new AppTargetId("widget:" + cn.getPackageName()),
+                    cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
+        } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+                && info.getTargetComponent() != null) {
+            ComponentName cn = info.getTargetComponent();
+            return new AppTarget.Builder(new AppTargetId("app:" + cn.getPackageName()),
+                    cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
+        } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+                && info instanceof WorkspaceItemInfo) {
+            ShortcutKey shortcutKey = ShortcutKey.fromItemInfo(info);
+            //TODO: switch to using full shortcut info
+            return new AppTarget.Builder(new AppTargetId("shortcut:" + shortcutKey.getId()),
+                    shortcutKey.componentName.getPackageName(), shortcutKey.user).build();
+        } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+            return new AppTarget.Builder(new AppTargetId("folder:" + info.id),
+                    mLauncher.getPackageName(), info.user).build();
+        }
+        return null;
+    }
+
+    private AppTargetEvent wrapAppTargetWithLocation(AppTarget target, int action, ItemInfo info) {
+        return wrapAppTargetWithLocation(target, action,
+                info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
+                        ? APP_LOCATION_HOTSEAT : APP_LOCATION_WORKSPACE, info.screenId, info.cellX,
+                info.cellY, info.spanX, info.spanY);
+    }
+
+    private AppTargetEvent wrapAppTargetWithLocation(AppTarget target, int action, String root,
+            int screenId, int x, int y, int spanX, int spanY) {
+        return new AppTargetEvent.Builder(target, action).setLaunchLocation(
+                String.format(Locale.ENGLISH, "%s/%d/[%d,%d]/[%d,%d]", root, screenId, x, y, spanX,
+                        spanY)).build();
+    }
+
+    /**
+     * A helper method to generate an AppTarget that's used to communicate workspace layout
+     */
+    private AppTarget getBlockAppTarget() {
+        return new AppTarget.Builder(new AppTargetId("block"),
+                mLauncher.getPackageName(), Process.myUserHandle()).build();
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 70880eb..8af26c6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -136,9 +136,11 @@
 
     @Override
     public void addSupportedAccessibilityActions(AccessibilityNodeInfo accessibilityNodeInfo) {
-        accessibilityNodeInfo.addAction(
-                new AccessibilityNodeInfo.AccessibilityAction(PIN_PREDICTION,
-                        getContext().getText(R.string.pin_prediction)));
+        if (!mIsPinned) {
+            accessibilityNodeInfo.addAction(
+                    new AccessibilityNodeInfo.AccessibilityAction(PIN_PREDICTION,
+                            getContext().getText(R.string.pin_prediction)));
+        }
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
index 70b139d..fadde37 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -204,27 +204,23 @@
                     true /* freezeTaskList */);
         } else {
             int taskId = mRecentsView.getNextPageTaskView().getTask().key.id;
-            mFinishingRecentsAnimationForNewTaskId = taskId;
-            mRecentsAnimationController.finish(true /* toRecents */, () -> {
-                if (!mCanceled) {
-                    TaskView nextTask = mRecentsView.getTaskView(taskId);
-                    if (nextTask != null) {
-                        nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
-                                success -> {
-                                    resultCallback.accept(success);
-                                    if (!success) {
-                                        mActivityInterface.onLaunchTaskFailed();
-                                        nextTask.notifyTaskLaunchFailed(TAG);
-                                    } else {
-                                        mActivityInterface.onLaunchTaskSuccess();
-                                    }
-                                }, MAIN_EXECUTOR.getHandler());
-                    }
-                    mStateCallback.setStateOnUiThread(successStateFlag);
+            if (!mCanceled) {
+                TaskView nextTask = mRecentsView.getTaskView(taskId);
+                if (nextTask != null) {
+                    nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
+                            success -> {
+                                resultCallback.accept(success);
+                                if (!success) {
+                                    mActivityInterface.onLaunchTaskFailed();
+                                    nextTask.notifyTaskLaunchFailed(TAG);
+                                } else {
+                                    mActivityInterface.onLaunchTaskSuccess();
+                                }
+                            }, MAIN_EXECUTOR.getHandler());
                 }
-                mCanceled = false;
-                mFinishingRecentsAnimationForNewTaskId = -1;
-            });
+                mStateCallback.setStateOnUiThread(successStateFlag);
+            }
+            mCanceled = false;
         }
         ActiveGestureLog.INSTANCE.addLog("finishRecentsAnimation", true);
     }
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 8574cf1..31a2814 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
@@ -30,6 +30,7 @@
 import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
 import static com.android.quickstep.GestureState.STATE_END_TARGET_ANIMATION_FINISHED;
 import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED;
+import static com.android.quickstep.GestureState.STATE_TASK_APPEARED_DURING_SWITCH;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
 import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.HIDE;
@@ -253,6 +254,8 @@
                         | STATE_RECENTS_SCROLLING_FINISHED,
                 this::onSettledOnEndTarget);
 
+        mGestureState.runOnceAtState(STATE_TASK_APPEARED_DURING_SWITCH, this::onTaskAppeared);
+
         mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED, this::invalidateHandler);
         mStateCallback.runOnceAtState(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
                 this::invalidateHandlerWithLauncher);
@@ -727,6 +730,22 @@
         }
     }
 
+    private void onTaskAppeared() {
+        RemoteAnimationTargetCompat app = mGestureState.getAnimationTarget();
+        if (mRecentsAnimationController != null && app != null) {
+
+            // TODO(b/152480470): Update Task target animation after onTaskAppeared holistically.
+            /* android.util.Log.d("LauncherSwipeHandler", "onTaskAppeared");
+
+            final boolean result = mRecentsAnimationController.removeTaskTarget(app);
+            mGestureState.setAnimationTarget(null);
+            android.util.Log.d("LauncherSwipeHandler", "removeTask, result=" + result); */
+
+            mRecentsAnimationController.finish(false /* toRecents */,
+                    null /* onFinishComplete */);
+        }
+    }
+
     private GestureEndTarget calculateEndTarget(PointF velocity, float endVelocity, boolean isFling,
             boolean isCancel) {
         final GestureEndTarget endTarget;
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 7979601..0fb51f3 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -58,7 +58,6 @@
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.model.AppLaunchTracker;
@@ -68,6 +67,7 @@
 import com.android.launcher3.tracing.nano.LauncherTraceProto;
 import com.android.launcher3.tracing.nano.TouchInteractionServiceProto;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
 import com.android.quickstep.inputconsumers.AssistantInputConsumer;
@@ -299,9 +299,6 @@
         if (mInputEventReceiver != null) {
             mInputEventReceiver.dispose();
             mInputEventReceiver = null;
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "disposeEventHandlers");
-            }
         }
         if (mInputMonitorCompat != null) {
             mInputMonitorCompat.dispose();
@@ -310,25 +307,16 @@
     }
 
     private void initInputMonitor() {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "initInputMonitor 1");
-        }
         disposeEventHandlers();
         if (mDeviceState.isButtonNavMode() || !SystemUiProxy.INSTANCE.get(this).isActive()) {
             return;
         }
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "initInputMonitor 2");
-        }
 
         Bundle bundle = SystemUiProxy.INSTANCE.get(this).monitorGestureInput("swipe-up",
                 mDeviceState.getDisplayId());
         mInputMonitorCompat = InputMonitorCompat.fromBundle(bundle, KEY_EXTRA_INPUT_MONITOR);
         mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(),
                 mMainChoreographer, this::onInputEvent);
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "initInputMonitor 3");
-        }
 
         mDeviceState.updateGestureTouchRegions();
     }
@@ -378,7 +366,7 @@
         if (!sharedPrefs.getBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true)) {
             sharedPrefs.edit()
                     .putBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true)
-                    .putBoolean(DiscoveryBounce.HOME_BOUNCE_SEEN, false)
+                    .putBoolean(OnboardingPrefs.HOME_BOUNCE_SEEN, false)
                     .apply();
         }
     }
@@ -453,9 +441,6 @@
     }
 
     private void onInputEvent(InputEvent ev) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "onInputEvent " + ev);
-        }
         if (!(ev instanceof MotionEvent)) {
             Log.e(TAG, "Unknown event " + ev);
             return;
@@ -539,6 +524,8 @@
         boolean canStartSystemGesture = mDeviceState.canStartSystemGesture();
 
         if (!mDeviceState.isUserUnlocked()) {
+            Log.d(TAG, "User locked. Can start system gesture? " + canStartSystemGesture
+                + " sysUiFlags: " + mDeviceState.getSystemUiStateFlags());
             if (canStartSystemGesture) {
                 // This handles apps launched in direct boot mode (e.g. dialer) as well as apps
                 // launched while device is locked even after exiting direct boot mode (e.g. camera).
diff --git a/quickstep/res/drawable-v28/back_gesture_tutorial_action_button_background.xml b/quickstep/res/drawable-v28/gesture_tutorial_action_button_background.xml
similarity index 91%
rename from quickstep/res/drawable-v28/back_gesture_tutorial_action_button_background.xml
rename to quickstep/res/drawable-v28/gesture_tutorial_action_button_background.xml
index cd30ef7..57423c2 100644
--- a/quickstep/res/drawable-v28/back_gesture_tutorial_action_button_background.xml
+++ b/quickstep/res/drawable-v28/gesture_tutorial_action_button_background.xml
@@ -16,5 +16,5 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle">
     <corners android:radius="?android:attr/dialogCornerRadius"/>
-    <solid android:color="@color/back_gesture_tutorial_primary_color"/>
+    <solid android:color="@color/gesture_tutorial_primary_color"/>
 </shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/back_gesture_tutorial_action_button_background.xml b/quickstep/res/drawable/gesture_tutorial_action_button_background.xml
similarity index 91%
rename from quickstep/res/drawable/back_gesture_tutorial_action_button_background.xml
rename to quickstep/res/drawable/gesture_tutorial_action_button_background.xml
index d7b9102..3f3b288 100644
--- a/quickstep/res/drawable/back_gesture_tutorial_action_button_background.xml
+++ b/quickstep/res/drawable/gesture_tutorial_action_button_background.xml
@@ -16,5 +16,5 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle">
     <corners android:radius="@dimen/default_dialog_corner_radius"/>
-    <solid android:color="@color/back_gesture_tutorial_primary_color"/>
+    <solid android:color="@color/gesture_tutorial_primary_color"/>
 </shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/back_gesture_tutorial_close_button.xml b/quickstep/res/drawable/gesture_tutorial_close_button.xml
similarity index 100%
rename from quickstep/res/drawable/back_gesture_tutorial_close_button.xml
rename to quickstep/res/drawable/gesture_tutorial_close_button.xml
diff --git a/quickstep/res/layout/back_gesture_tutorial_activity.xml b/quickstep/res/layout/gesture_tutorial_activity.xml
similarity index 92%
rename from quickstep/res/layout/back_gesture_tutorial_activity.xml
rename to quickstep/res/layout/gesture_tutorial_activity.xml
index e894e89..4dc8913 100644
--- a/quickstep/res/layout/back_gesture_tutorial_activity.xml
+++ b/quickstep/res/layout/gesture_tutorial_activity.xml
@@ -14,6 +14,6 @@
     limitations under the License.
 -->
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/back_gesture_tutorial_fragment_container"
+    android:id="@+id/gesture_tutorial_fragment_container"
     android:layout_width="match_parent"
     android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/quickstep/res/layout/back_gesture_tutorial_fragment.xml b/quickstep/res/layout/gesture_tutorial_fragment.xml
similarity index 71%
rename from quickstep/res/layout/back_gesture_tutorial_fragment.xml
rename to quickstep/res/layout/gesture_tutorial_fragment.xml
index d8c25bd..0bc062a 100644
--- a/quickstep/res/layout/back_gesture_tutorial_fragment.xml
+++ b/quickstep/res/layout/gesture_tutorial_fragment.xml
@@ -16,27 +16,27 @@
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:background="@color/back_gesture_tutorial_background_color">
+    android:background="@color/gesture_tutorial_background_color">
 
     <ImageView
-        android:id="@+id/back_gesture_tutorial_fragment_hand_coaching"
+        android:id="@+id/gesture_tutorial_fragment_hand_coaching"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:scaleType="centerCrop"/>
 
     <ImageButton
-        android:id="@+id/back_gesture_tutorial_fragment_close_button"
+        android:id="@+id/gesture_tutorial_fragment_close_button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:padding="18dp"
         android:layout_marginTop="30dp"
         android:layout_marginStart="4dp"
-        android:layout_alignParentLeft="true"
+        android:layout_alignParentStart="true"
         android:layout_alignParentTop="true"
         android:background="@android:color/transparent"
-        android:accessibilityTraversalAfter="@id/back_gesture_tutorial_fragment_titles_container"
-        android:contentDescription="@string/back_gesture_tutorial_close_button_content_description"
-        android:src="@drawable/back_gesture_tutorial_close_button"/>
+        android:accessibilityTraversalAfter="@id/gesture_tutorial_fragment_titles_container"
+        android:contentDescription="@string/gesture_tutorial_close_button_content_description"
+        android:src="@drawable/gesture_tutorial_close_button"/>
 
     <LinearLayout
         android:layout_width="match_parent"
@@ -45,29 +45,29 @@
         android:orientation="vertical">
 
         <LinearLayout
-            android:id="@+id/back_gesture_tutorial_fragment_titles_container"
+            android:id="@+id/gesture_tutorial_fragment_titles_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:orientation="vertical"
             android:focusable="true">
 
             <TextView
-                android:id="@+id/back_gesture_tutorial_fragment_title_view"
+                android:id="@+id/gesture_tutorial_fragment_title_view"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_gravity="center_horizontal"
-                android:layout_marginStart="@dimen/back_gesture_tutorial_title_margin_start_end"
-                android:layout_marginEnd="@dimen/back_gesture_tutorial_title_margin_start_end"
+                android:layout_marginStart="@dimen/gesture_tutorial_title_margin_start_end"
+                android:layout_marginEnd="@dimen/gesture_tutorial_title_margin_start_end"
                 style="@style/TextAppearance.BackGestureTutorial.Title"/>
 
             <TextView
-                android:id="@+id/back_gesture_tutorial_fragment_subtitle_view"
+                android:id="@+id/gesture_tutorial_fragment_subtitle_view"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_gravity="center_horizontal"
                 android:layout_marginTop="10dp"
-                android:layout_marginStart="@dimen/back_gesture_tutorial_subtitle_margin_start_end"
-                android:layout_marginEnd="@dimen/back_gesture_tutorial_subtitle_margin_start_end"
+                android:layout_marginStart="@dimen/gesture_tutorial_subtitle_margin_start_end"
+                android:layout_marginEnd="@dimen/gesture_tutorial_subtitle_margin_start_end"
                 style="@style/TextAppearance.BackGestureTutorial.Subtitle"/>
 
         </LinearLayout>
@@ -91,21 +91,21 @@
             android:layout_gravity="center_horizontal">
 
             <Button
-                android:id="@+id/back_gesture_tutorial_fragment_action_button"
+                android:id="@+id/gesture_tutorial_fragment_action_button"
                 android:layout_width="142dp"
                 android:layout_height="49dp"
-                android:layout_marginEnd="@dimen/back_gesture_tutorial_button_margin_start_end"
+                android:layout_marginEnd="@dimen/gesture_tutorial_button_margin_start_end"
                 android:layout_alignParentEnd="true"
                 android:stateListAnimator="@null"
-                android:background="@drawable/back_gesture_tutorial_action_button_background"
+                android:background="@drawable/gesture_tutorial_action_button_background"
                 android:foreground="?android:attr/selectableItemBackgroundBorderless"
                 style="@style/TextAppearance.BackGestureTutorial.ButtonLabel"/>
 
             <Button
-                android:id="@+id/back_gesture_tutorial_fragment_action_text_button"
+                android:id="@+id/gesture_tutorial_fragment_action_text_button"
                 android:layout_width="142dp"
                 android:layout_height="49dp"
-                android:layout_marginStart="@dimen/back_gesture_tutorial_button_margin_start_end"
+                android:layout_marginStart="@dimen/gesture_tutorial_button_margin_start_end"
                 android:layout_alignParentStart="true"
                 android:stateListAnimator="@null"
                 android:background="@null"
@@ -113,7 +113,5 @@
                 style="@style/TextAppearance.BackGestureTutorial.TextButtonLabel"/>
 
         </RelativeLayout>
-
     </LinearLayout>
-
 </RelativeLayout>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 9a61165..18dc19c 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -81,7 +81,7 @@
     <dimen name="gestures_overscroll_fling_threshold">40dp</dimen>
 
     <!-- Tips Gesture Tutorial -->
-    <dimen name="back_gesture_tutorial_title_margin_start_end">40dp</dimen>
-    <dimen name="back_gesture_tutorial_subtitle_margin_start_end">16dp</dimen>
-    <dimen name="back_gesture_tutorial_button_margin_start_end">18dp</dimen>
+    <dimen name="gesture_tutorial_title_margin_start_end">40dp</dimen>
+    <dimen name="gesture_tutorial_subtitle_margin_start_end">16dp</dimen>
+    <dimen name="gesture_tutorial_button_margin_start_end">18dp</dimen>
 </resources>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index c6b1477..d7c976d 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -61,7 +61,7 @@
     <string name="all_apps_prediction_tip">Your predicted apps</string>
 
     <!-- Content description for a close button. [CHAR LIMIT=NONE] -->
-    <string  name="back_gesture_tutorial_close_button_content_description" translatable="false">Close</string>
+    <string  name="gesture_tutorial_close_button_content_description" translatable="false">Close</string>
 
 
     <!-- Hotseat migration notification title -->
@@ -110,9 +110,9 @@
     <string name="back_gesture_tutorial_confirm_subtitle" translatable="false">To change the sensitivity of the back gesture, go to Settings</string>
 
     <!-- Button text shown on a button on the confirm screen. [CHAR LIMIT=14] -->
-    <string name="back_gesture_tutorial_action_button_label" translatable="false">Done</string>
+    <string name="gesture_tutorial_action_button_label" translatable="false">Done</string>
     <!-- Button text shown on a text button on the confirm screen. [CHAR LIMIT=14] -->
-    <string name="back_gesture_tutorial_action_text_button_label" translatable="false">Settings</string>
+    <string name="gesture_tutorial_action_text_button_label" translatable="false">Settings</string>
 
     <!-- ******* Overview ******* -->
     <!-- Label for a button that causes the current overview app to be shared. [CHAR_LIMIT=40] -->
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index bf107fb..14e054e 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -35,14 +35,14 @@
     <style name="TextAppearance.BackGestureTutorial.Title"
         parent="TextAppearance.BackGestureTutorial">
         <item name="android:gravity">center</item>
-        <item name="android:textColor">@color/back_gesture_tutorial_title_color</item>
+        <item name="android:textColor">@color/gesture_tutorial_title_color</item>
         <item name="android:textSize">28sp</item>
     </style>
 
     <style name="TextAppearance.BackGestureTutorial.Subtitle"
         parent="TextAppearance.BackGestureTutorial">
         <item name="android:gravity">center</item>
-        <item name="android:textColor">@color/back_gesture_tutorial_subtitle_color</item>
+        <item name="android:textColor">@color/gesture_tutorial_subtitle_color</item>
         <item name="android:letterSpacing">0.03</item>
         <item name="android:textSize">21sp</item>
     </style>
@@ -50,7 +50,7 @@
     <style name="TextAppearance.BackGestureTutorial.ButtonLabel"
         parent="TextAppearance.BackGestureTutorial.CallToAction">
         <item name="android:gravity">center</item>
-        <item name="android:textColor">@color/back_gesture_tutorial_action_button_label_color</item>
+        <item name="android:textColor">@color/gesture_tutorial_action_button_label_color</item>
         <item name="android:letterSpacing">0.02</item>
         <item name="android:textSize">16sp</item>
         <item name="android:textAllCaps">false</item>
@@ -58,7 +58,7 @@
 
     <style name="TextAppearance.BackGestureTutorial.TextButtonLabel"
         parent="TextAppearance.BackGestureTutorial.ButtonLabel">
-        <item name="android:textColor">@color/back_gesture_tutorial_primary_color</item>
+        <item name="android:textColor">@color/gesture_tutorial_primary_color</item>
     </style>
 
     <style name="OverviewActionButton"
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 69400b3..48e25bd 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -17,14 +17,7 @@
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
 import static com.android.launcher3.AbstractFloatingView.TYPE_HIDE_BACK_BUTTON;
-import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.allapps.DiscoveryBounce.BOUNCE_MAX_COUNT;
-import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_COUNT;
-import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN;
-import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_COUNT;
-import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_SEEN;
 import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
 
@@ -32,6 +25,7 @@
 import android.animation.ValueAnimator;
 import android.content.Intent;
 import android.content.IntentSender;
+import android.content.SharedPreferences;
 import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.view.View;
@@ -47,12 +41,14 @@
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.uioverrides.RecentsViewStateController;
+import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.UiThreadHelper;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.QuickstepOnboardingPrefs;
 import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.util.RemoteFadeOutAnimationListener;
 import com.android.quickstep.util.ShelfPeekAnim;
@@ -86,45 +82,6 @@
         super.onCreate(savedInstanceState);
 
         SysUINavigationMode.INSTANCE.get(this).addModeChangeListener(this);
-
-        if (!getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)) {
-            getStateManager().addStateListener(new LauncherStateManager.StateListener() {
-                @Override
-                public void onStateTransitionStart(LauncherState toState) { }
-
-                @Override
-                public void onStateTransitionComplete(LauncherState finalState) {
-                    boolean swipeUpEnabled = SysUINavigationMode.INSTANCE
-                            .get(BaseQuickstepLauncher.this).getMode().hasGestures;
-                    LauncherState prevState = getStateManager().getLastState();
-
-                    if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled
-                            && finalState == ALL_APPS && prevState == NORMAL) || BOUNCE_MAX_COUNT
-                            <= getSharedPrefs().getInt(HOME_BOUNCE_COUNT, 0))) {
-                        getSharedPrefs().edit().putBoolean(HOME_BOUNCE_SEEN, true).apply();
-                        getStateManager().removeStateListener(this);
-                    }
-                }
-            });
-        }
-
-        if (!getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false)) {
-            getStateManager().addStateListener(new LauncherStateManager.StateListener() {
-                @Override
-                public void onStateTransitionStart(LauncherState toState) { }
-
-                @Override
-                public void onStateTransitionComplete(LauncherState finalState) {
-                    LauncherState prevState = getStateManager().getLastState();
-
-                    if ((finalState == ALL_APPS && prevState == OVERVIEW) || BOUNCE_MAX_COUNT
-                            <= getSharedPrefs().getInt(SHELF_BOUNCE_COUNT, 0)) {
-                        getSharedPrefs().edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply();
-                        getStateManager().removeStateListener(this);
-                    }
-                }
-            });
-        }
     }
 
     @Override
@@ -244,6 +201,12 @@
     }
 
     @Override
+    protected OnboardingPrefs createOnboardingPrefs(SharedPreferences sharedPrefs,
+            LauncherStateManager stateManager) {
+        return new QuickstepOnboardingPrefs(this, sharedPrefs, stateManager);
+    }
+
+    @Override
     protected ScaleAndTranslation getOverviewScaleAndTranslationForNormalState() {
         if (SysUINavigationMode.getMode(this) == Mode.NO_BUTTON) {
             PagedOrientationHandler layoutVertical =
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index 123c988..0f45196 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -62,10 +62,13 @@
 
     @Override
     public void setState(@NonNull LauncherState state) {
-        ScaleAndTranslation scaleAndTranslation = state
-                .getOverviewScaleAndTranslation(mLauncher);
+        ScaleAndTranslation scaleAndTranslation = state.getOverviewScaleAndTranslation(mLauncher);
+        float translationX = scaleAndTranslation.translationX;
+        if (mRecentsView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+            translationX = -translationX;
+        }
         SCALE_PROPERTY.set(mRecentsView, scaleAndTranslation.scale);
-        mRecentsView.setTranslationX(scaleAndTranslation.translationX);
+        mRecentsView.setTranslationX(translationX);
         mRecentsView.setTranslationY(scaleAndTranslation.translationY);
 
         getContentAlphaProperty().set(mRecentsView, state.overviewUi ? 1f : 0);
@@ -96,9 +99,13 @@
     void setStateWithAnimationInternal(@NonNull final LauncherState toState,
             @NonNull StateAnimationConfig config, @NonNull PendingAnimation setter) {
         ScaleAndTranslation scaleAndTranslation = toState.getOverviewScaleAndTranslation(mLauncher);
+        float translationX = scaleAndTranslation.translationX;
+        if (mRecentsView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+            translationX = -translationX;
+        }
         setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndTranslation.scale,
                 config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
-        setter.setFloat(mRecentsView, VIEW_TRANSLATE_X, scaleAndTranslation.translationX,
+        setter.setFloat(mRecentsView, VIEW_TRANSLATE_X, translationX,
                 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
         setter.setFloat(mRecentsView, VIEW_TRANSLATE_Y, scaleAndTranslation.translationY,
                 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 5118906..544f420 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -23,6 +23,7 @@
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -109,6 +110,9 @@
     public static final int STATE_RECENTS_SCROLLING_FINISHED =
             getFlagForIndex("STATE_RECENTS_SCROLLING_FINISHED");
 
+    // Called when the new task appeared from quick switching.
+    public static final int STATE_TASK_APPEARED_DURING_SWITCH =
+            getFlagForIndex("STATE_TASK_APPEARED_DURING_SWITCH");
 
     // Needed to interact with the current activity
     private final Intent mHomeIntent;
@@ -119,6 +123,7 @@
 
     private ActivityManager.RunningTaskInfo mRunningTask;
     private GestureEndTarget mEndTarget;
+    private RemoteAnimationTargetCompat mAnimationTarget;
     // TODO: This can be removed once we stop finishing the animation when starting a new task
     private int mFinishingRecentsAnimationTaskId = -1;
 
@@ -227,6 +232,14 @@
         return mEndTarget;
     }
 
+    public void setAnimationTarget(RemoteAnimationTargetCompat target) {
+        mAnimationTarget = target;
+    }
+
+    public RemoteAnimationTargetCompat getAnimationTarget() {
+        return mAnimationTarget;
+    }
+
     /**
      * Sets the end target of this gesture and immediately notifies the state changes.
      */
@@ -301,6 +314,12 @@
         mStateCallback.setState(STATE_RECENTS_ANIMATION_ENDED);
     }
 
+    @Override
+    public void onTaskAppeared(RemoteAnimationTargetCompat app) {
+        mAnimationTarget = app;
+        mStateCallback.setState(STATE_TASK_APPEARED_DURING_SWITCH);
+    }
+
     public void dump(PrintWriter pw) {
         pw.println("GestureState:");
         pw.println("  gestureID=" + mGestureId);
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 10f9feb..b0ce8e6 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -124,6 +124,17 @@
     }
 
     @Override
+    public void onRecentTaskListUpdated() {
+        // In some cases immediately after booting, the tasks in the system recent task list may be
+        // loaded, but not in the active task hierarchy in the system.  These tasks are displayed in 
+        // overview, but removing them don't result in a onTaskStackChanged() nor a onTaskRemoved()
+        // callback (those are for changes to the active tasks), but the task list is still updated,
+        // so we should also invalidate the change id to ensure we load a new list instead of 
+        // reusing a stale list.
+        mChangeId++;
+    }
+
+    @Override
     public void onTaskRemoved(int taskId) {
         mTasks = loadTasksInBackground(Integer.MAX_VALUE, false);
     }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index d1dbcfb..7d568a4 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -118,6 +118,16 @@
         });
     }
 
+    @BinderThread
+    @Override
+    public void onTaskAppeared(RemoteAnimationTargetCompat app) {
+        Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
+            for (RecentsAnimationListener listener : getListeners()) {
+                listener.onTaskAppeared(app);
+            }
+        });
+    }
+
     private final void onAnimationFinished(RecentsAnimationController controller) {
         Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), () -> {
             for (RecentsAnimationListener listener : getListeners()) {
@@ -147,5 +157,10 @@
          * Callback made whenever the recents animation is finished.
          */
         default void onRecentsAnimationFinished(RecentsAnimationController controller) {}
+
+        /**
+         * Callback made when a task started from the recents is ready for an app transition.
+         */
+        default void onTaskAppeared(RemoteAnimationTargetCompat app) {}
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 8dd4aa4..5ece2d7 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -28,12 +28,14 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.UiThread;
 
 import com.android.launcher3.util.Preconditions;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
 import java.util.function.Consumer;
 import java.util.function.Supplier;
@@ -107,6 +109,15 @@
         UI_HELPER_EXECUTOR.execute(() -> mController.cleanupScreenshot());
     }
 
+    /**
+     * Remove task remote animation target from
+     * {@link RecentsAnimationCallbacks#onTaskAppeared(RemoteAnimationTargetCompat)}}.
+     */
+    @UiThread
+    public boolean removeTaskTarget(@NonNull RemoteAnimationTargetCompat target) {
+        return mController.removeTask(target.taskId);
+    }
+
     @UiThread
     public void finishAnimationToHome() {
         finishAndClear(true /* toRecents */, null, false /* sendUserLeaveHint */);
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 8cb27a3..0a00a61 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -74,6 +74,8 @@
         NavigationModeChangeListener,
         DefaultDisplay.DisplayInfoChangeListener {
 
+    private static final String TAG = "RecentsAnimationDeviceState";
+
     private final Context mContext;
     private final SysUINavigationMode mSysUiNavMode;
     private final DefaultDisplay mDefaultDisplay;
@@ -96,6 +98,7 @@
         @Override
         public void onReceive(Context context, Intent intent) {
             if (ACTION_USER_UNLOCKED.equals(intent.getAction())) {
+                Log.d(TAG, "User Unlocked Broadcast Received");
                 mIsUserUnlocked = true;
                 notifyUserUnlocked();
             }
@@ -212,9 +215,6 @@
 
     @Override
     public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_BACKGROUND_TO_OVERVIEW_TAG, "onNavigationModeChanged " + newMode);
-        }
         mDefaultDisplay.removeChangeListener(this);
         if (newMode.hasGestures) {
             mDefaultDisplay.addChangeListener(this);
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialConfirmController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialConfirmController.java
deleted file mode 100644
index 486d676..0000000
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialConfirmController.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.interaction;
-
-import android.view.View;
-
-import com.android.launcher3.R;
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
-
-import java.util.Optional;
-
-/**
- * An implementation of {@link BackGestureTutorialController} that defines the behavior of the
- * {@link TutorialStep#CONFIRM}.
- */
-final class BackGestureTutorialConfirmController extends BackGestureTutorialController {
-
-    BackGestureTutorialConfirmController(BackGestureTutorialFragment fragment,
-            BackGestureTutorialTypeInfo tutorialTypeInfo) {
-        super(fragment, TutorialStep.CONFIRM, Optional.of(tutorialTypeInfo));
-    }
-
-    @Override
-    Optional<Integer> getTitleStringId() {
-        return Optional.of(mTutorialTypeInfo.get().getTutorialConfirmTitleId());
-    }
-
-    @Override
-    Optional<Integer> getSubtitleStringId() {
-        return Optional.of(mTutorialTypeInfo.get().getTutorialConfirmSubtitleId());
-    }
-
-    @Override
-    Optional<Integer> getActionButtonStringId() {
-        return Optional.of(R.string.back_gesture_tutorial_action_button_label);
-    }
-
-    @Override
-    Optional<Integer> getActionTextButtonStringId() {
-        return Optional.of(R.string.back_gesture_tutorial_action_text_button_label);
-    }
-
-    @Override
-    void onActionButtonClicked(View button) {
-        hideHandCoachingAnimation();
-        if (button == mActionTextButton) {
-            mFragment.startSystemNavigationSetting();
-        }
-        mFragment.closeTutorial();
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
index 5c2e992..640ae76 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -15,164 +15,101 @@
  */
 package com.android.quickstep.interaction;
 
+import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION_COMPLETE;
+import static com.android.quickstep.interaction.TutorialController.TutorialType.LEFT_EDGE_BACK_NAVIGATION;
+
 import android.view.View;
-import android.widget.Button;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
 
 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;
+/** A {@link TutorialController} for the Back tutorial. */
+final class BackGestureTutorialController extends TutorialController {
 
-/**
- * Defines the behavior of the particular {@link TutorialStep} and implements the transition to it.
- */
-abstract class BackGestureTutorialController {
-
-    final BackGestureTutorialFragment mFragment;
-    final TutorialStep mTutorialStep;
-    final Optional<BackGestureTutorialTypeInfo> mTutorialTypeInfo;
-    final Button mActionTextButton;
-    final Button mActionButton;
-    final TextView mSubtitleTextView;
-    final ImageButton mCloseButton;
-    final BackGestureTutorialHandAnimation mHandCoachingAnimation;
-    final LinearLayout mTitlesContainer;
-
-    private final TextView mTitleTextView;
-    private final ImageView mHandCoachingView;
-
-    BackGestureTutorialController(
-            BackGestureTutorialFragment fragment,
-            TutorialStep tutorialStep,
-            Optional<BackGestureTutorialTypeInfo> tutorialTypeInfo) {
-        mFragment = fragment;
-        mTutorialStep = tutorialStep;
-        mTutorialTypeInfo = tutorialTypeInfo;
-
-        View rootView = fragment.getRootView();
-        mActionTextButton = rootView.findViewById(
-                R.id.back_gesture_tutorial_fragment_action_text_button);
-        mActionButton = rootView.findViewById(R.id.back_gesture_tutorial_fragment_action_button);
-        mSubtitleTextView = rootView.findViewById(
-                R.id.back_gesture_tutorial_fragment_subtitle_view);
-        mTitleTextView = rootView.findViewById(R.id.back_gesture_tutorial_fragment_title_view);
-        mHandCoachingView = rootView.findViewById(
-                R.id.back_gesture_tutorial_fragment_hand_coaching);
-        mHandCoachingAnimation = mFragment.getHandAnimation();
-        mHandCoachingView.bringToFront();
-        mCloseButton = rootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button);
-        mTitlesContainer = rootView.findViewById(
-                R.id.back_gesture_tutorial_fragment_titles_container);
+    BackGestureTutorialController(BackGestureTutorialFragment fragment, TutorialType tutorialType) {
+        super(fragment, tutorialType);
     }
 
+    @Override
     void transitToController() {
-        updateTitles();
-        updateActionButtons();
+        super.transitToController();
+        if (mTutorialType != BACK_NAVIGATION_COMPLETE) {
+            mHandCoachingAnimation.startLoopedAnimation(mTutorialType);
+        }
     }
 
-    void hideHandCoachingAnimation() {
-        mHandCoachingAnimation.stop();
+    @Override
+    Integer getTitleStringId() {
+        switch (mTutorialType) {
+            case RIGHT_EDGE_BACK_NAVIGATION:
+                return R.string.back_gesture_tutorial_playground_title_swipe_inward_right_edge;
+            case LEFT_EDGE_BACK_NAVIGATION:
+                return R.string.back_gesture_tutorial_playground_title_swipe_inward_left_edge;
+            case BACK_NAVIGATION_COMPLETE:
+                return R.string.back_gesture_tutorial_confirm_title;
+        }
+        return null;
     }
 
-    void onGestureAttempted(BackGestureResult result) {
-        if (mTutorialStep == TutorialStep.CONFIRM
-                && (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
-                    || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT)) {
-            mFragment.closeTutorial();
-            return;
+    @Override
+    Integer getSubtitleStringId() {
+        switch (mTutorialType) {
+            case RIGHT_EDGE_BACK_NAVIGATION:
+                return R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_right_edge;
+            case LEFT_EDGE_BACK_NAVIGATION:
+                return R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_left_edge;
+            case BACK_NAVIGATION_COMPLETE:
+                return R.string.back_gesture_tutorial_confirm_subtitle;
         }
+        return null;
+    }
 
-        if (!mTutorialTypeInfo.isPresent()) {
-            return;
+    @Override
+    Integer getActionButtonStringId() {
+        if (mTutorialType == BACK_NAVIGATION_COMPLETE) {
+            return R.string.gesture_tutorial_action_button_label;
         }
+        return null;
+    }
 
-        switch (mTutorialTypeInfo.get().getTutorialType()) {
+    @Override
+    Integer getActionTextButtonStringId() {
+        if (mTutorialType == BACK_NAVIGATION_COMPLETE) {
+            return R.string.gesture_tutorial_action_text_button_label;
+        }
+        return null;
+    }
+
+    @Override
+    void onActionButtonClicked(View button) {
+        hideHandCoachingAnimation();
+        if (button == mActionTextButton) {
+            mTutorialFragment.startSystemNavigationSetting();
+        }
+        mTutorialFragment.closeTutorial();
+    }
+
+    @Override
+    public void onBackGestureAttempted(BackGestureResult result) {
+        switch (mTutorialType) {
             case RIGHT_EDGE_BACK_NAVIGATION:
                 if (result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
                     hideHandCoachingAnimation();
-                    mFragment.changeController(
-                            TutorialStep.ENGAGED, TutorialType.LEFT_EDGE_BACK_NAVIGATION);
+                    mTutorialFragment.changeController(LEFT_EDGE_BACK_NAVIGATION);
                 }
                 break;
             case LEFT_EDGE_BACK_NAVIGATION:
                 if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT) {
                     hideHandCoachingAnimation();
-                    mFragment.changeController(TutorialStep.CONFIRM);
+                    mTutorialFragment.changeController(BACK_NAVIGATION_COMPLETE);
                 }
                 break;
-        }
-    }
-
-    abstract Optional<Integer> getTitleStringId();
-
-    abstract Optional<Integer> getSubtitleStringId();
-
-    abstract Optional<Integer> getActionButtonStringId();
-
-    abstract Optional<Integer> getActionTextButtonStringId();
-
-    abstract void onActionButtonClicked(View button);
-
-    private void updateActionButtons() {
-        updateButton(mActionButton, getActionButtonStringId(), this::onActionButtonClicked);
-        updateButton(mActionTextButton, getActionTextButtonStringId(), this::onActionButtonClicked);
-    }
-
-    private static void updateButton(Button button, Optional<Integer> stringId,
-            View.OnClickListener listener) {
-        if (!stringId.isPresent()) {
-            button.setVisibility(View.INVISIBLE);
-            return;
-        }
-
-        button.setVisibility(View.VISIBLE);
-        button.setText(stringId.get());
-        button.setOnClickListener(listener);
-    }
-
-    private void updateTitles() {
-        updateTitleView(mTitleTextView, getTitleStringId(),
-                R.style.TextAppearance_BackGestureTutorial_Title);
-        updateTitleView(mSubtitleTextView, getSubtitleStringId(),
-                R.style.TextAppearance_BackGestureTutorial_Subtitle);
-    }
-
-    private static void updateTitleView(TextView textView, Optional<Integer> stringId,
-            int styleId) {
-        if (!stringId.isPresent()) {
-            textView.setVisibility(View.GONE);
-            return;
-        }
-
-        textView.setVisibility(View.VISIBLE);
-        textView.setText(stringId.get());
-        textView.setTextAppearance(styleId);
-    }
-
-    /**
-     * Constructs {@link BackGestureTutorialController} for providing {@link TutorialType} and
-     * {@link TutorialStep}.
-     */
-    static Optional<BackGestureTutorialController> getTutorialController(
-            BackGestureTutorialFragment fragment, TutorialStep tutorialStep,
-            TutorialType tutorialType) {
-        BackGestureTutorialTypeInfo tutorialTypeInfo =
-                BackGestureTutorialTypeInfoProvider.getTutorialTypeInfo(tutorialType);
-        switch (tutorialStep) {
-            case ENGAGED:
-                return Optional.of(
-                        new BackGestureTutorialEngagedController(fragment, tutorialTypeInfo));
-            case CONFIRM:
-                return Optional.of(
-                        new BackGestureTutorialConfirmController(fragment, tutorialTypeInfo));
-            default:
-                throw new AssertionError("Unexpected tutorial step: " + tutorialStep);
+            case BACK_NAVIGATION_COMPLETE:
+                if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
+                        || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
+                    mTutorialFragment.closeTutorial();
+                }
+                break;
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialEngagedController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialEngagedController.java
deleted file mode 100644
index c9ee1e2..0000000
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialEngagedController.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.interaction;
-
-import android.view.View;
-
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
-
-import java.util.Optional;
-
-/**
- * An implementation of {@link BackGestureTutorialController} that defines the behavior of the
- * {@link TutorialStep#ENGAGED}.
- */
-final class BackGestureTutorialEngagedController extends BackGestureTutorialController {
-
-    BackGestureTutorialEngagedController(
-            BackGestureTutorialFragment fragment, BackGestureTutorialTypeInfo tutorialTypeInfo) {
-        super(fragment, TutorialStep.ENGAGED, Optional.of(tutorialTypeInfo));
-    }
-
-    @Override
-    void transitToController() {
-        super.transitToController();
-        mHandCoachingAnimation.maybeStartLoopedAnimation(mTutorialTypeInfo.get().getTutorialType());
-    }
-
-    @Override
-    Optional<Integer> getTitleStringId() {
-        return Optional.of(mTutorialTypeInfo.get().getTutorialPlaygroundTitleId());
-    }
-
-    @Override
-    Optional<Integer> getSubtitleStringId() {
-        return Optional.of(mTutorialTypeInfo.get().getTutorialEngagedSubtitleId());
-    }
-
-    @Override
-    Optional<Integer> getActionButtonStringId() {
-        return Optional.empty();
-    }
-
-    @Override
-    Optional<Integer> getActionTextButtonStringId() {
-        return Optional.empty();
-    }
-
-    @Override
-    void onActionButtonClicked(View button) {
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
index aeb718d..ddf1cda 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
@@ -15,172 +15,43 @@
  */
 package com.android.quickstep.interaction;
 
-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;
+import com.android.quickstep.interaction.TutorialController.TutorialType;
 
 /** Shows the Back gesture interactive tutorial. */
-public class BackGestureTutorialFragment extends Fragment implements BackGestureAttemptCallback {
-
-    private static final String LOG_TAG = "TutorialFragment";
-    private static final String KEY_TUTORIAL_STEP = "tutorialStep";
-    private static final String KEY_TUTORIAL_TYPE = "tutorialType";
-    private static final String SYSTEM_NAVIGATION_SETTING_INTENT =
-            "#Intent;action=com.android.settings.SEARCH_RESULT_TRAMPOLINE;S"
-                    + ".:settings:fragment_args_key=gesture_system_navigation_input_summary;S"
-                    + ".:settings:show_fragment=com.android.settings.gestures"
-                    + ".SystemNavigationGestureSettings;end";
-
-    private TutorialStep mTutorialStep;
-    private TutorialType mTutorialType;
-    private Optional<BackGestureTutorialController> mTutorialController = Optional.empty();
-    private View mRootView;
-    private BackGestureTutorialHandAnimation mHandCoachingAnimation;
-    private EdgeBackGestureHandler mEdgeBackGestureHandler;
-
-    public static BackGestureTutorialFragment newInstance(
-            TutorialStep tutorialStep, TutorialType tutorialType) {
-        BackGestureTutorialFragment fragment = new BackGestureTutorialFragment();
-        Bundle args = new Bundle();
-        args.putSerializable(KEY_TUTORIAL_STEP, tutorialStep);
-        args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType);
-        fragment.setArguments(args);
-        return fragment;
-    }
+public class BackGestureTutorialFragment extends TutorialFragment
+        implements BackGestureAttemptCallback {
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        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(
-            @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;
+    public void onDestroy() {
+        super.onDestroy();
+        mEdgeBackGestureHandler.unregisterBackGestureAttemptCallback();
     }
 
     @Override
-    public void onResume() {
-        super.onResume();
-        changeController(mTutorialStep, mTutorialType);
+    int getHandAnimationResId() {
+        return R.drawable.back_gesture;
     }
 
     @Override
-    public void onPause() {
-        super.onPause();
-        mHandCoachingAnimation.stop();
-    }
-
-    void onAttachedToWindow() {
-        mEdgeBackGestureHandler.setViewGroupParent((ViewGroup) getRootView());
-    }
-
-    void onDetachedFromWindow() {
-        mEdgeBackGestureHandler.setViewGroupParent(null);
-    }
-
-    @Override
-    public void onSaveInstanceState(Bundle savedInstanceState) {
-        savedInstanceState.putSerializable(KEY_TUTORIAL_STEP, mTutorialStep);
-        savedInstanceState.putSerializable(KEY_TUTORIAL_TYPE, mTutorialType);
-        super.onSaveInstanceState(savedInstanceState);
-    }
-
-    View getRootView() {
-        return mRootView;
-    }
-
-    BackGestureTutorialHandAnimation getHandAnimation() {
-        return mHandCoachingAnimation;
-    }
-
-    void changeController(TutorialStep tutorialStep) {
-        changeController(tutorialStep, mTutorialType);
-    }
-
-    void changeController(TutorialStep tutorialStep, TutorialType tutorialType) {
-        Optional<BackGestureTutorialController> tutorialController =
-                BackGestureTutorialController.getTutorialController(/* fragment= */ this,
-                        tutorialStep, tutorialType);
-        if (!tutorialController.isPresent()) {
-            return;
-        }
-
-        mTutorialController = tutorialController;
-        mTutorialController.get().transitToController();
-        this.mTutorialStep = mTutorialController.get().mTutorialStep;
-        this.mTutorialType = tutorialType;
+    TutorialController createController(TutorialType type) {
+        return new BackGestureTutorialController(this, type);
     }
 
     @Override
     public void onBackGestureAttempted(BackGestureResult result) {
-        mTutorialController.ifPresent(controller -> controller.onGestureAttempted(result));
-    }
-
-    void closeTutorial() {
-        getActivity().finish();
-    }
-
-    void startSystemNavigationSetting() {
-        try {
-            startActivityForResult(
-                    Intent.parseUri(SYSTEM_NAVIGATION_SETTING_INTENT, /* flags= */ 0),
-                    /* requestCode= */ 0);
-        } catch (URISyntaxException e) {
-            Log.e(LOG_TAG, "The launch Intent Uri is wrong syntax: " + e);
-        } catch (ActivityNotFoundException e) {
-            Log.e(LOG_TAG, "The launch Activity not found: " + e);
+        if (mTutorialController != null) {
+            mTutorialController.onBackGestureAttempted(result);
         }
     }
-
-    private void onCloseButtonClicked(View button) {
-        closeTutorial();
-    }
-
-    /** Denotes the step of the tutorial. */
-    enum TutorialStep {
-        ENGAGED,
-        CONFIRM,
-    }
-
-    /** Denotes the type of the tutorial. */
-    enum TutorialType {
-        RIGHT_EDGE_BACK_NAVIGATION,
-        LEFT_EDGE_BACK_NAVIGATION,
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfo.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfo.java
deleted file mode 100644
index ac8443d..0000000
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfo.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.interaction;
-
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
-
-/** Defines the UI element identifiers for the particular {@link TutorialType}. */
-final class BackGestureTutorialTypeInfo {
-
-    private final TutorialType mTutorialType;
-    private final int mTutorialPlaygroundTitleId;
-    private final int mTutorialEngagedSubtitleId;
-    private final int mTutorialConfirmTitleId;
-    private final int mTutorialConfirmSubtitleId;
-
-    TutorialType getTutorialType() {
-        return mTutorialType;
-    }
-
-    int getTutorialPlaygroundTitleId() {
-        return mTutorialPlaygroundTitleId;
-    }
-
-    int getTutorialEngagedSubtitleId() {
-        return mTutorialEngagedSubtitleId;
-    }
-
-    int getTutorialConfirmTitleId() {
-        return mTutorialConfirmTitleId;
-    }
-
-    int getTutorialConfirmSubtitleId() {
-        return mTutorialConfirmSubtitleId;
-    }
-
-    static Builder builder() {
-        return new Builder();
-    }
-
-    private BackGestureTutorialTypeInfo(
-            TutorialType tutorialType,
-            int tutorialPlaygroundTitleId,
-            int tutorialEngagedSubtitleId,
-            int tutorialConfirmTitleId,
-            int tutorialConfirmSubtitleId) {
-        mTutorialType = tutorialType;
-        mTutorialPlaygroundTitleId = tutorialPlaygroundTitleId;
-        mTutorialEngagedSubtitleId = tutorialEngagedSubtitleId;
-        mTutorialConfirmTitleId = tutorialConfirmTitleId;
-        mTutorialConfirmSubtitleId = tutorialConfirmSubtitleId;
-    }
-
-    /** Builder for producing {@link BackGestureTutorialTypeInfo} objects. */
-    static class Builder {
-
-        private TutorialType mTutorialType;
-        private Integer mTutorialPlaygroundTitleId;
-        private Integer mTutorialEngagedSubtitleId;
-        private Integer mTutorialConfirmTitleId;
-        private Integer mTutorialConfirmSubtitleId;
-
-        Builder setTutorialType(TutorialType tutorialType) {
-            mTutorialType = tutorialType;
-            return this;
-        }
-
-        Builder setTutorialPlaygroundTitleId(int stringId) {
-            mTutorialPlaygroundTitleId = stringId;
-            return this;
-        }
-
-        Builder setTutorialEngagedSubtitleId(int stringId) {
-            mTutorialEngagedSubtitleId = stringId;
-            return this;
-        }
-
-        Builder setTutorialConfirmTitleId(int stringId) {
-            mTutorialConfirmTitleId = stringId;
-            return this;
-        }
-
-        Builder setTutorialConfirmSubtitleId(int stringId) {
-            mTutorialConfirmSubtitleId = stringId;
-            return this;
-        }
-
-        BackGestureTutorialTypeInfo build() {
-            return new BackGestureTutorialTypeInfo(
-                    mTutorialType,
-                    mTutorialPlaygroundTitleId,
-                    mTutorialEngagedSubtitleId,
-                    mTutorialConfirmTitleId,
-                    mTutorialConfirmSubtitleId);
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfoProvider.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfoProvider.java
deleted file mode 100644
index 9575d83..0000000
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfoProvider.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.interaction;
-
-import com.android.launcher3.R;
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
-
-/** Provides instances of {@link BackGestureTutorialTypeInfo} for each {@link TutorialType}. */
-final class BackGestureTutorialTypeInfoProvider {
-
-    private static final BackGestureTutorialTypeInfo RIGHT_EDGE_BACK_NAV_TUTORIAL_INFO =
-            BackGestureTutorialTypeInfo.builder()
-                    .setTutorialType(TutorialType.RIGHT_EDGE_BACK_NAVIGATION)
-                    .setTutorialPlaygroundTitleId(
-                            R.string.back_gesture_tutorial_playground_title_swipe_inward_right_edge)
-                    .setTutorialEngagedSubtitleId(
-                            R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_right_edge)
-                    .setTutorialConfirmTitleId(R.string.back_gesture_tutorial_confirm_title)
-                    .setTutorialConfirmSubtitleId(R.string.back_gesture_tutorial_confirm_subtitle)
-                    .build();
-
-    private static final BackGestureTutorialTypeInfo LEFT_EDGE_BACK_NAV_TUTORIAL_INFO =
-            BackGestureTutorialTypeInfo.builder()
-                    .setTutorialType(TutorialType.LEFT_EDGE_BACK_NAVIGATION)
-                    .setTutorialPlaygroundTitleId(
-                            R.string.back_gesture_tutorial_playground_title_swipe_inward_left_edge)
-                    .setTutorialEngagedSubtitleId(
-                            R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_left_edge)
-                    .setTutorialConfirmTitleId(R.string.back_gesture_tutorial_confirm_title)
-                    .setTutorialConfirmSubtitleId(R.string.back_gesture_tutorial_confirm_subtitle)
-                    .build();
-
-    static BackGestureTutorialTypeInfo getTutorialTypeInfo(TutorialType tutorialType) {
-        switch (tutorialType) {
-            case RIGHT_EDGE_BACK_NAVIGATION:
-                return RIGHT_EDGE_BACK_NAV_TUTORIAL_INFO;
-            case LEFT_EDGE_BACK_NAVIGATION:
-                return LEFT_EDGE_BACK_NAV_TUTORIAL_INFO;
-            default:
-                throw new AssertionError("Unexpected tutorial type: " + tutorialType);
-        }
-    }
-
-    private BackGestureTutorialTypeInfoProvider() {
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
index f34530e..89c57a0 100644
--- a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
@@ -20,7 +20,6 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.os.SystemProperties;
-import android.view.Display;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnTouchListener;
@@ -46,7 +45,6 @@
     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;
@@ -91,8 +89,6 @@
     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,
@@ -126,6 +122,10 @@
         mGestureCallback = callback;
     }
 
+    void unregisterBackGestureAttemptCallback() {
+        mGestureCallback = null;
+    }
+
     private LayoutParams createLayoutParams() {
         Resources resources = mContext.getResources();
         return new LayoutParams(
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
index 4815366..414ddfa 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -19,6 +19,7 @@
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.util.DisplayMetrics;
+import android.util.Log;
 import android.view.Display;
 import android.view.View;
 import android.view.Window;
@@ -26,26 +27,32 @@
 import androidx.fragment.app.FragmentActivity;
 
 import com.android.launcher3.R;
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+import com.android.quickstep.interaction.TutorialController.TutorialType;
 
 import java.util.List;
 
 /** Shows the gesture interactive sandbox in full screen mode. */
 public class GestureSandboxActivity extends FragmentActivity {
 
-    private BackGestureTutorialFragment mFragment;
+    private static final String LOG_TAG = "GestureSandboxActivity";
+
+    private TutorialFragment mFragment;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         requestWindowFeature(Window.FEATURE_NO_TITLE);
-        setContentView(R.layout.back_gesture_tutorial_activity);
+        setContentView(R.layout.gesture_tutorial_activity);
 
-        mFragment = BackGestureTutorialFragment.newInstance(
-            TutorialStep.ENGAGED, TutorialType.RIGHT_EDGE_BACK_NAVIGATION);
+        try {
+            mFragment = TutorialFragment.newInstance(BackGestureTutorialFragment.class,
+                    TutorialType.RIGHT_EDGE_BACK_NAVIGATION);
+        } catch (InstantiationException | IllegalAccessException e) {
+            Log.wtf(LOG_TAG, "Failed to create tutorial fragment!", e);
+            mFragment = new BackGestureTutorialFragment();
+        }
         getSupportFragmentManager().beginTransaction()
-                .add(R.id.back_gesture_tutorial_fragment_container, mFragment)
+                .add(R.id.gesture_tutorial_fragment_container, mFragment)
                 .commit();
     }
 
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
new file mode 100644
index 0000000..cd4d0d8
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -0,0 +1,135 @@
+/*
+ * 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.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
+
+abstract class TutorialController {
+
+    final TutorialFragment mTutorialFragment;
+    final TutorialType mTutorialType;
+
+    final ImageButton mCloseButton;
+    final TextView mTitleTextView;
+    final TextView mSubtitleTextView;
+    final TutorialHandAnimation mHandCoachingAnimation;
+    final ImageView mHandCoachingView;
+    final Button mActionTextButton;
+    final Button mActionButton;
+
+    TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
+        mTutorialFragment = tutorialFragment;
+        mTutorialType = tutorialType;
+
+        View rootView = tutorialFragment.getRootView();
+        mCloseButton = rootView.findViewById(R.id.gesture_tutorial_fragment_close_button);
+        mCloseButton.setOnClickListener(button -> mTutorialFragment.closeTutorial());
+        mTitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_title_view);
+        mSubtitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_subtitle_view);
+        mHandCoachingAnimation = tutorialFragment.getHandAnimation();
+        mHandCoachingView = rootView.findViewById(R.id.gesture_tutorial_fragment_hand_coaching);
+        mHandCoachingView.bringToFront();
+        mActionTextButton =
+                rootView.findViewById(R.id.gesture_tutorial_fragment_action_text_button);
+        mActionButton = rootView.findViewById(R.id.gesture_tutorial_fragment_action_button);
+    }
+
+    abstract void onBackGestureAttempted(BackGestureResult result);
+
+    @Nullable
+    Integer getTitleStringId() {
+        return null;
+    }
+
+    @Nullable
+    Integer getSubtitleStringId() {
+        return null;
+    }
+
+    @Nullable
+    Integer getActionButtonStringId() {
+        return null;
+    }
+
+    @Nullable
+    Integer getActionTextButtonStringId() {
+        return null;
+    }
+
+    void onActionButtonClicked(View button) {}
+
+    void hideHandCoachingAnimation() {
+        mHandCoachingAnimation.stop();
+    }
+
+    @CallSuper
+    void transitToController() {
+        updateTitles();
+        updateActionButtons();
+    }
+
+    private void updateTitles() {
+        updateTitleView(mTitleTextView, getTitleStringId(),
+                R.style.TextAppearance_BackGestureTutorial_Title);
+        updateTitleView(mSubtitleTextView, getSubtitleStringId(),
+                R.style.TextAppearance_BackGestureTutorial_Subtitle);
+    }
+
+    private void updateTitleView(TextView textView, @Nullable Integer stringId, int styleId) {
+        if (stringId == null) {
+            textView.setVisibility(View.GONE);
+            return;
+        }
+
+        textView.setVisibility(View.VISIBLE);
+        textView.setText(stringId);
+        textView.setTextAppearance(styleId);
+    }
+
+    private void updateActionButtons() {
+        updateButton(mActionButton, getActionButtonStringId(), this::onActionButtonClicked);
+        updateButton(mActionTextButton, getActionTextButtonStringId(), this::onActionButtonClicked);
+    }
+
+    private void updateButton(Button button, @Nullable Integer stringId, OnClickListener listener) {
+        if (stringId == null) {
+            button.setVisibility(View.INVISIBLE);
+            return;
+        }
+
+        button.setVisibility(View.VISIBLE);
+        button.setText(stringId);
+        button.setOnClickListener(listener);
+    }
+
+    /** Denotes the type of the tutorial. */
+    enum TutorialType {
+        RIGHT_EDGE_BACK_NAVIGATION,
+        LEFT_EDGE_BACK_NAVIGATION,
+        BACK_NAVIGATION_COMPLETE,
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
new file mode 100644
index 0000000..09ea8d6
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
@@ -0,0 +1,153 @@
+/*
+ * 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.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.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.TutorialController.TutorialType;
+
+import java.net.URISyntaxException;
+
+abstract class TutorialFragment extends Fragment {
+
+    private static final String LOG_TAG = "TutorialFragment";
+    private static final String SYSTEM_NAVIGATION_SETTING_INTENT =
+            "#Intent;action=com.android.settings.SEARCH_RESULT_TRAMPOLINE;S"
+                    + ".:settings:fragment_args_key=gesture_system_navigation_input_summary;S"
+                    + ".:settings:show_fragment=com.android.settings.gestures"
+                    + ".SystemNavigationGestureSettings;end";
+    private static final String KEY_TUTORIAL_TYPE = "tutorialType";
+
+    TutorialType mTutorialType;
+    @Nullable TutorialController mTutorialController = null;
+    View mRootView;
+    TutorialHandAnimation mHandCoachingAnimation;
+    EdgeBackGestureHandler mEdgeBackGestureHandler;
+
+    public static TutorialFragment newInstance(
+            Class<? extends TutorialFragment> fragmentClass, TutorialType tutorialType)
+            throws java.lang.InstantiationException, IllegalAccessException {
+        TutorialFragment fragment = fragmentClass.newInstance();
+        Bundle args = new Bundle();
+        args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType);
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    abstract int getHandAnimationResId();
+
+    abstract TutorialController createController(TutorialType type);
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
+        mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
+        mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext());
+    }
+
+    @Override
+    public View onCreateView(
+            @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        super.onCreateView(inflater, container, savedInstanceState);
+
+        mRootView = inflater.inflate(R.layout.gesture_tutorial_fragment,
+                container, /* attachToRoot= */ false);
+        mRootView.setOnApplyWindowInsetsListener((view, insets) -> {
+            Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());
+            mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right);
+            return insets;
+        });
+        mRootView.setOnTouchListener(mEdgeBackGestureHandler);
+        mHandCoachingAnimation = new TutorialHandAnimation(getContext(), mRootView,
+                getHandAnimationResId());
+        return mRootView;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        changeController(mTutorialType);
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mHandCoachingAnimation.stop();
+    }
+
+    void onAttachedToWindow() {
+        mEdgeBackGestureHandler.setViewGroupParent((ViewGroup) getRootView());
+    }
+
+    void onDetachedFromWindow() {
+        mEdgeBackGestureHandler.setViewGroupParent(null);
+    }
+
+    void changeController(TutorialType tutorialType) {
+        mTutorialController = createController(tutorialType);
+        mTutorialController.transitToController();
+        mTutorialType = tutorialType;
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle savedInstanceState) {
+        savedInstanceState.putSerializable(KEY_TUTORIAL_TYPE, mTutorialType);
+        super.onSaveInstanceState(savedInstanceState);
+    }
+
+    View getRootView() {
+        return mRootView;
+    }
+
+    TutorialHandAnimation getHandAnimation() {
+        return mHandCoachingAnimation;
+    }
+
+    void closeTutorial() {
+        FragmentActivity activity = getActivity();
+        if (activity != null) {
+            activity.finish();
+        }
+    }
+
+    void startSystemNavigationSetting() {
+        try {
+            startActivityForResult(
+                    Intent.parseUri(SYSTEM_NAVIGATION_SETTING_INTENT, /* flags= */ 0),
+                    /* requestCode= */ 0);
+        } catch (URISyntaxException e) {
+            Log.e(LOG_TAG, "The launch Intent Uri is wrong syntax: " + e);
+        } catch (ActivityNotFoundException e) {
+            Log.e(LOG_TAG, "The launch Activity not found: " + e);
+        }
+    }
+
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialHandAnimation.java b/quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java
similarity index 68%
rename from quickstep/src/com/android/quickstep/interaction/BackGestureTutorialHandAnimation.java
rename to quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java
index d03811d..5362aaf 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialHandAnimation.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialHandAnimation.java
@@ -25,12 +25,12 @@
 import androidx.core.content.ContextCompat;
 
 import com.android.launcher3.R;
-import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+import com.android.quickstep.interaction.TutorialController.TutorialType;
 
 import java.time.Duration;
 
 /** Hand coaching animation. */
-final class BackGestureTutorialHandAnimation {
+final class TutorialHandAnimation {
 
     // A delay for waiting the Activity fully launches.
     private static final Duration ANIMATION_START_DELAY = Duration.ofMillis(300L);
@@ -38,29 +38,18 @@
     private final ImageView mHandCoachingView;
     private final AnimatedVectorDrawable mGestureAnimation;
 
-    private boolean mIsAnimationPlayed = false;
-
-    BackGestureTutorialHandAnimation(Context context, View rootView) {
-        mHandCoachingView = rootView.findViewById(
-                R.id.back_gesture_tutorial_fragment_hand_coaching);
-        mGestureAnimation = (AnimatedVectorDrawable) ContextCompat.getDrawable(context,
-                R.drawable.back_gesture);
+    TutorialHandAnimation(Context context, View rootView, int resId) {
+        mHandCoachingView = rootView.findViewById(R.id.gesture_tutorial_fragment_hand_coaching);
+        mGestureAnimation = (AnimatedVectorDrawable) ContextCompat.getDrawable(context, resId);
     }
 
-    boolean isRunning() {
-        return mGestureAnimation.isRunning();
-    }
-
-    /**
-     * Starts animation if the playground is launched for the first time.
-     */
-    void maybeStartLoopedAnimation(TutorialType tutorialType) {
-        if (isRunning() || mIsAnimationPlayed) {
-            return;
+    /** [Re]starts animation for the given tutorial. */
+    void startLoopedAnimation(TutorialType tutorialType) {
+        if (mGestureAnimation.isRunning()) {
+            stop();
         }
 
-        mIsAnimationPlayed = true;
-        clearAnimationCallbacks();
+        mGestureAnimation.clearAnimationCallbacks();
         mGestureAnimation.registerAnimationCallback(
                 new Animatable2.AnimationCallback() {
                     @Override
@@ -78,17 +67,11 @@
         float rotationY = tutorialType == TutorialType.LEFT_EDGE_BACK_NAVIGATION ? 180f : 0f;
         mHandCoachingView.setRotationY(rotationY);
         mHandCoachingView.setImageDrawable(mGestureAnimation);
-        mHandCoachingView.postDelayed(() -> mGestureAnimation.start(),
-                ANIMATION_START_DELAY.toMillis());
-    }
-
-    private void clearAnimationCallbacks() {
-        mGestureAnimation.clearAnimationCallbacks();
+        mHandCoachingView.postDelayed(mGestureAnimation::start, ANIMATION_START_DELAY.toMillis());
     }
 
     void stop() {
-        mIsAnimationPlayed = false;
-        clearAnimationCallbacks();
+        mGestureAnimation.clearAnimationCallbacks();
         mGestureAnimation.stop();
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
new file mode 100644
index 0000000..ab2484d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.util;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+
+import android.content.SharedPreferences;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager;
+import com.android.launcher3.LauncherStateManager.StateListener;
+import com.android.launcher3.util.OnboardingPrefs;
+import com.android.quickstep.SysUINavigationMode;
+
+/**
+ * Extends {@link OnboardingPrefs} for quickstep-specific onboarding data.
+ */
+public class QuickstepOnboardingPrefs extends OnboardingPrefs<BaseQuickstepLauncher> {
+
+    public QuickstepOnboardingPrefs(BaseQuickstepLauncher launcher, SharedPreferences sharedPrefs,
+            LauncherStateManager stateManager) {
+        super(launcher, sharedPrefs, stateManager);
+
+        if (!getBoolean(HOME_BOUNCE_SEEN)) {
+            mStateManager.addStateListener(new StateListener() {
+                @Override
+                public void onStateTransitionStart(LauncherState toState) { }
+
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    boolean swipeUpEnabled = SysUINavigationMode.INSTANCE
+                            .get(mLauncher).getMode().hasGestures;
+                    LauncherState prevState = mStateManager.getLastState();
+
+                    if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled
+                            && finalState == ALL_APPS && prevState == NORMAL) ||
+                            hasReachedMaxCount(HOME_BOUNCE_COUNT))) {
+                        mSharedPrefs.edit().putBoolean(HOME_BOUNCE_SEEN, true).apply();
+                        mStateManager.removeStateListener(this);
+                    }
+                }
+            });
+        }
+
+        if (!getBoolean(SHELF_BOUNCE_SEEN)) {
+            mStateManager.addStateListener(new StateListener() {
+                @Override
+                public void onStateTransitionStart(LauncherState toState) { }
+
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    LauncherState prevState = mStateManager.getLastState();
+
+                    if ((finalState == ALL_APPS && prevState == OVERVIEW) ||
+                            hasReachedMaxCount(SHELF_BOUNCE_COUNT)) {
+                        mSharedPrefs.edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply();
+                        mStateManager.removeStateListener(this);
+                    }
+                }
+            });
+        }
+
+        if (!hasReachedMaxCount(ALL_APPS_COUNT)) {
+            mStateManager.addStateListener(new StateListener() {
+                @Override
+                public void onStateTransitionStart(LauncherState toState) { }
+
+                @Override
+                public void onStateTransitionComplete(LauncherState finalState) {
+                    if (finalState == ALL_APPS) {
+                        if (incrementEventCount(ALL_APPS_COUNT)) {
+                            mStateManager.removeStateListener(this);
+                            mLauncher.getScrimView().updateDragHandleVisibility();
+                        }
+                    }
+                }
+            });
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
index c2ccd90..f5498c9 100644
--- a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
+++ b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
@@ -43,6 +43,7 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.uioverrides.states.OverviewState;
+import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.SysUINavigationMode;
@@ -75,6 +76,7 @@
     private final float mRadius;
     private final int mMaxScrimAlpha;
     private final Paint mPaint;
+    private final OnboardingPrefs mOnboardingPrefs;
 
     // Mid point where the alpha changes
     private int mMidAlpha;
@@ -100,6 +102,7 @@
     private boolean mRemainingScreenPathValid = false;
 
     private Mode mSysUINavigationMode;
+    private boolean mIsTwoZoneSwipeModel;
 
     public ShelfScrimView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -108,6 +111,7 @@
         mEndAlpha = Color.alpha(mEndScrim);
         mRadius = BOTTOM_CORNER_RADIUS_RATIO * Themes.getDialogCornerRadius(context);
         mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mOnboardingPrefs = mLauncher.getOnboardingPrefs();
 
         // Just assume the easiest UI for now, until we have the proper layout information.
         mDrawingFlatColor = true;
@@ -140,9 +144,11 @@
             // Show the shelf more quickly before reaching overview progress.
             mBeforeMidProgressColorInterpolator = ACCEL_2;
             mAfterMidProgressColorInterpolator = ACCEL;
+            mIsTwoZoneSwipeModel = FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get();
         } else {
             mBeforeMidProgressColorInterpolator = ACCEL;
             mAfterMidProgressColorInterpolator = Interpolators.clampToProgress(ACCEL, 0.5f, 1f);
+            mIsTwoZoneSwipeModel = false;
         }
     }
 
@@ -236,9 +242,9 @@
 
     @Override
     protected boolean shouldDragHandleBeVisible() {
-        boolean twoZoneSwipeModel = FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()
-                && SysUINavigationMode.removeShelfFromOverview(mLauncher);
-        return twoZoneSwipeModel || super.shouldDragHandleBeVisible();
+        boolean needsAllAppsEdu = mIsTwoZoneSwipeModel
+                && !mOnboardingPrefs.hasReachedMaxCount(OnboardingPrefs.ALL_APPS_COUNT);
+        return needsAllAppsEdu || super.shouldDragHandleBeVisible();
     }
 
     @Override
diff --git a/res/layout/work_apps_paused.xml b/res/layout/work_apps_paused.xml
index f64b2d9..08e1c98 100644
--- a/res/layout/work_apps_paused.xml
+++ b/res/layout/work_apps_paused.xml
@@ -20,7 +20,7 @@
     android:gravity="center">
 
     <TextView
-        style="@style/PrimaryMediumText"
+        style="@style/PrimaryHeadline"
         android:textColor="?attr/workProfileOverlayTextColor"
         android:id="@+id/work_apps_paused_title"
         android:layout_width="wrap_content"
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 5a15ec6..e946835 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -118,6 +118,8 @@
         <!-- numHotseatIcons defaults to numColumns, if not specified -->
         <attr name="numHotseatIcons" format="integer" />
         <attr name="dbFile" format="string" />
+        <!-- numAllAppsColumns defaults to numColumns, if not specified -->
+        <attr name="numAllAppsColumns" format="integer" />
         <attr name="defaultLayoutId" format="reference" />
         <attr name="demoModeLayoutId" format="reference" />
     </declare-styleable>
@@ -133,6 +135,12 @@
         <attr name="iconTextSize" format="float" />
         <!-- If true, this display option is used to determine the default grid -->
         <attr name="canBeDefault" format="boolean" />
+
+        <!-- The following values are only enabled if grid is supported. -->
+        <!-- allAppsIconSize defaults to iconSize, if not specified -->
+        <attr name="allAppsIconSize" format="float" />
+        <!-- allAppsIconTextSize defaults to iconTextSize, if not specified -->
+        <attr name="allAppsIconTextSize" format="float" />
     </declare-styleable>
 
     <declare-styleable name="CellLayout">
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 194ef2c..adcff9a 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -38,11 +38,11 @@
     <color name="all_apps_bg_hand_fill">#E5E5E5</color>
     <color name="all_apps_bg_hand_fill_dark">#9AA0A6</color>
 
-    <color name="back_gesture_tutorial_background_color">#FFFFFFFF</color>
-    <color name="back_gesture_tutorial_subtitle_color">#99000000</color> <!-- 60% black -->
-    <color name="back_gesture_tutorial_title_color">#FF000000</color>
-    <color name="back_gesture_tutorial_action_button_label_color">#FFFFFFFF</color>
-    <color name="back_gesture_tutorial_primary_color">#1A73E8</color> <!-- Blue -->
+    <color name="gesture_tutorial_background_color">#FFFFFFFF</color>
+    <color name="gesture_tutorial_subtitle_color">#99000000</color> <!-- 60% black -->
+    <color name="gesture_tutorial_title_color">#FF000000</color>
+    <color name="gesture_tutorial_action_button_label_color">#FFFFFFFF</color>
+    <color name="gesture_tutorial_primary_color">#1A73E8</color> <!-- Blue -->
 
 
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ac04262..65e0816 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -207,7 +207,7 @@
     <!-- Summary for Notification dots setting. Tapping this will link enable/disable notification dots feature on the home screen. [CHAR LIMIT=50] -->
     <string name="notification_dots_service_title">Show notification dots</string>
 
-    <!-- Label for the setting that allows the automatic placement of launcher shortcuts for applications and games installed on the device [CHAR LIMIT=40] -->
+    <!-- Label for the setting that allows the automatic placement of launcher shortcuts for applications and games installed on the device [CHAR LIMIT=60] -->
     <string name="auto_add_shortcuts_label">Add app icons to Home screen</string>
     <!-- Text description of the setting that allows the automatic placement of launcher shortcuts for applications and games installed on the device [CHAR LIMIT=NONE] -->
     <string name="auto_add_shortcuts_description">For new apps</string>
@@ -343,6 +343,10 @@
     <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>
+    <!-- content description for paused work apps list -->
+    <string name="work_apps_paused_content_description">Work profile is paused. Work apps can\’t send you notifications, use your battery, or access your location</string>
+
+
 
     <!-- A tip shown pointing at work toggle -->
     <string name="work_switch_tip">Pause work apps and notifications</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index bc6ab45..fcc651b 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -226,6 +226,9 @@
 
     <style name="TextHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle" />
     <style name="PrimaryMediumText" parent="@android:style/TextAppearance.DeviceDefault.Medium"/>
+    <style name="PrimaryHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle">
+        <item name="android:textStyle">bold</item>
+    </style>
 
     <style name="TextTitle" parent="@android:style/TextAppearance.DeviceDefault" />
 
diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java
index 9e8441a..41eeb78 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -16,17 +16,21 @@
 
 package com.android.launcher3;
 
+import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
+
 import android.content.Context;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 
 
@@ -187,4 +191,28 @@
             AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext());
         }
     }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        if (isLayoutSuppressed()) info.setScrollable(false);
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS,
+                    "onInitializeAccessibilityNodeInfo, scrollable: " + info.isScrollable());
+        }
+    }
+
+    @Override
+    public void setLayoutFrozen(boolean frozen) {
+        final boolean changing = frozen != isLayoutSuppressed();
+        super.setLayoutFrozen(frozen);
+        if (changing) {
+            if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+                Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "setLayoutFrozen " + frozen
+                        + " @ " + Log.getStackTraceString(new Throwable()));
+                ActivityContext.lookupContext(getContext()).getDragLayer()
+                        .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED);
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 4e1e586..fc3c9fd 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -25,8 +25,6 @@
 import android.util.DisplayMetrics;
 import android.view.Surface;
 
-import androidx.annotation.Nullable;
-
 import com.android.launcher3.CellLayout.ContainerType;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.icons.DotRenderer;
@@ -36,8 +34,6 @@
 public class DeviceProfile {
 
     public final InvariantDeviceProfile inv;
-    // IDP with no grid override values.
-    @Nullable private final InvariantDeviceProfile originalIdp;
 
     // Device properties
     public final boolean isTablet;
@@ -138,11 +134,10 @@
     public DotRenderer mDotRendererAllApps;
 
     public DeviceProfile(Context context, InvariantDeviceProfile inv,
-            InvariantDeviceProfile originalIDP, Point minSize, Point maxSize,
+            Point minSize, Point maxSize,
             int width, int height, boolean isLandscape, boolean isMultiWindowMode) {
 
         this.inv = inv;
-        this.originalIdp = inv;
         this.isLandscape = isLandscape;
         this.isMultiWindowMode = isMultiWindowMode;
 
@@ -234,19 +229,6 @@
             // Recalculate the available dimensions using the new hotseat size.
             updateAvailableDimensions(dm, res);
         }
-
-        if (originalIDP != null) {
-            // Grid size change should not affect All Apps UI, so we use the original profile
-            // measurements here.
-            DeviceProfile originalProfile = isLandscape
-                    ? originalIDP.landscapeProfile
-                    : originalIDP.portraitProfile;
-            allAppsIconSizePx = originalProfile.iconSizePx;
-            allAppsIconTextSizePx = originalProfile.iconTextSizePx;
-            allAppsCellHeightPx = originalProfile.allAppsCellHeightPx;
-            allAppsIconDrawablePaddingPx = originalProfile.iconDrawablePaddingOriginalPx;
-            allAppsCellWidthPx = allAppsIconSizePx + 2 * allAppsIconDrawablePaddingPx;
-        }
         updateWorkspacePadding();
 
         // This is done last, after iconSizePx is calculated above.
@@ -259,8 +241,8 @@
 
     public DeviceProfile copy(Context context) {
         Point size = new Point(availableWidthPx, availableHeightPx);
-        return new DeviceProfile(context, inv, originalIdp, size, size, widthPx, heightPx,
-                isLandscape, isMultiWindowMode);
+        return new DeviceProfile(context, inv, size, size, widthPx, heightPx, isLandscape,
+                isMultiWindowMode);
     }
 
     public DeviceProfile getMultiWindowProfile(Context context, Point mwSize) {
@@ -271,8 +253,8 @@
         // In multi-window mode, we can have widthPx = availableWidthPx
         // and heightPx = availableHeightPx because Launcher uses the InvariantDeviceProfiles'
         // widthPx and heightPx values where it's needed.
-        DeviceProfile profile = new DeviceProfile(context, inv, null, mwSize, mwSize,
-                mwSize.x, mwSize.y, isLandscape, true);
+        DeviceProfile profile = new DeviceProfile(context, inv, mwSize, mwSize, mwSize.x, mwSize.y,
+                isLandscape, true);
 
         // If there isn't enough vertical cell padding with the labels displayed, hide the labels.
         float workspaceCellPaddingY = profile.getCellSize().y - profile.iconSizePx
@@ -356,11 +338,19 @@
         }
         cellWidthPx = iconSizePx + iconDrawablePaddingPx;
 
-        allAppsIconSizePx = iconSizePx;
-        allAppsIconTextSizePx = iconTextSizePx;
-        allAppsIconDrawablePaddingPx = iconDrawablePaddingPx;
-        allAppsCellHeightPx = getCellSize().y;
-        allAppsCellWidthPx = allAppsIconSizePx + 2 * allAppsIconDrawablePaddingPx;
+        // All apps
+        if (allAppsHasDifferentNumColumns()) {
+            allAppsIconSizePx = ResourceUtils.pxFromDp(inv.allAppsIconSize, dm);
+            allAppsIconTextSizePx = Utilities.pxFromSp(inv.allAppsIconTextSize, dm);
+            allAppsCellHeightPx = getCellSize(inv.numAllAppsColumns, inv.numAllAppsColumns).y;
+            allAppsIconDrawablePaddingPx = iconDrawablePaddingOriginalPx;
+        } else {
+            allAppsIconSizePx = iconSizePx;
+            allAppsIconTextSizePx = iconTextSizePx;
+            allAppsIconDrawablePaddingPx = iconDrawablePaddingPx;
+            allAppsCellHeightPx = getCellSize().y;
+        }
+        allAppsCellWidthPx = allAppsIconSizePx + allAppsIconDrawablePaddingPx;
 
         if (isVerticalBarLayout()) {
             // Always hide the Workspace text with vertical bar layout.
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 7414a88..d2d0863 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -47,7 +47,6 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.graphics.IconShape;
-import com.android.launcher3.graphics.LauncherPreviewRenderer;
 import com.android.launcher3.util.ConfigMonitor;
 import com.android.launcher3.util.DefaultDisplay;
 import com.android.launcher3.util.DefaultDisplay.Info;
@@ -61,7 +60,6 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 
 public class InvariantDeviceProfile {
 
@@ -107,6 +105,8 @@
     public int iconBitmapSize;
     public int fillResIconDpi;
     public float iconTextSize;
+    public float allAppsIconSize;
+    public float allAppsIconTextSize;
 
     private SparseArray<TypedValue> mExtraAttrs;
 
@@ -149,6 +149,8 @@
         numHotseatIcons = p.numHotseatIcons;
         numAllAppsColumns = p.numAllAppsColumns;
         dbFile = p.dbFile;
+        allAppsIconSize = p.allAppsIconSize;
+        allAppsIconTextSize = p.allAppsIconTextSize;
         defaultLayoutId = p.defaultLayoutId;
         demoModeLayoutId = p.demoModeLayoutId;
         mExtraAttrs = p.mExtraAttrs;
@@ -157,12 +159,9 @@
 
     @TargetApi(23)
     private InvariantDeviceProfile(Context context) {
-        if (context instanceof LauncherPreviewRenderer.PreviewContext) {
-            throw new IllegalArgumentException(
-                    "PreviewContext is passed into this IDP constructor");
-        }
-
-        String gridName = getCurrentGridName(context);
+        String gridName = Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)
+                ? Utilities.getPrefs(context).getString(KEY_IDP_GRID_NAME, null)
+                : null;
         initGrid(context, gridName);
         mConfigMonitor = new ConfigMonitor(context,
                 APPLY_CONFIG_AT_RUNTIME.get() ? this::onConfigChanged : this::killProcess);
@@ -179,7 +178,7 @@
         }
     }
 
-    /**
+/**
      * This constructor should NOT have any monitors by design.
      */
     public InvariantDeviceProfile(Context context, Display display) {
@@ -211,11 +210,55 @@
         Point smallestSize = new Point(displayInfo.smallestSize);
         Point largestSize = new Point(displayInfo.largestSize);
 
+        ArrayList<DisplayOption> allOptions = getPredefinedDeviceProfiles(context, gridName);
         // This guarantees that width < height
         float minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y),
                 displayInfo.metrics);
         float minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y),
                 displayInfo.metrics);
+        // Sort the profiles based on the closeness to the device size
+        Collections.sort(allOptions, (a, b) ->
+                Float.compare(dist(minWidthDps, minHeightDps, a.minWidthDps, a.minHeightDps),
+                        dist(minWidthDps, minHeightDps, b.minWidthDps, b.minHeightDps)));
+        DisplayOption interpolatedDisplayOption =
+                invDistWeightedInterpolate(minWidthDps,  minHeightDps, allOptions);
+
+        GridOption closestProfile = allOptions.get(0).grid;
+        numRows = closestProfile.numRows;
+        numColumns = closestProfile.numColumns;
+        numHotseatIcons = closestProfile.numHotseatIcons;
+        dbFile = closestProfile.dbFile;
+        defaultLayoutId = closestProfile.defaultLayoutId;
+        demoModeLayoutId = closestProfile.demoModeLayoutId;
+        numFolderRows = closestProfile.numFolderRows;
+        numFolderColumns = closestProfile.numFolderColumns;
+        numAllAppsColumns = closestProfile.numAllAppsColumns;
+
+        mExtraAttrs = closestProfile.extraAttrs;
+
+        if (!closestProfile.name.equals(gridName)) {
+            Utilities.getPrefs(context).edit()
+                    .putString(KEY_IDP_GRID_NAME, closestProfile.name).apply();
+        }
+
+        iconSize = interpolatedDisplayOption.iconSize;
+        iconShapePath = getIconShapePath(context);
+        landscapeIconSize = interpolatedDisplayOption.landscapeIconSize;
+        iconBitmapSize = ResourceUtils.pxFromDp(iconSize, displayInfo.metrics);
+        iconTextSize = interpolatedDisplayOption.iconTextSize;
+        fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
+
+        if (Utilities.getPrefs(context).getBoolean(GRID_OPTIONS_PREFERENCE_KEY, false)) {
+            allAppsIconSize = interpolatedDisplayOption.allAppsIconSize;
+            allAppsIconTextSize = interpolatedDisplayOption.allAppsIconTextSize;
+        } else {
+            allAppsIconSize = iconSize;
+            allAppsIconTextSize = iconTextSize;
+        }
+
+        // If the partner customization apk contains any grid overrides, apply them
+        // Supported overrides: numRows, numColumns, iconSize
+        applyPartnerDeviceProfileOverrides(context, displayInfo.metrics);
 
         Point realSize = new Point(displayInfo.realSize);
         // The real size never changes. smallSide and largeSide will remain the
@@ -223,64 +266,10 @@
         int smallSide = Math.min(realSize.x, realSize.y);
         int largeSide = Math.max(realSize.x, realSize.y);
 
-        // We want a list of all options as well as the list of filtered options. This allows us
-        // to have a consistent UI for areas that the grid size change should not affect
-        // ie. All Apps should be consistent between grid sizes.
-        ArrayList<DisplayOption> allOptions = new ArrayList<>();
-        ArrayList<DisplayOption> filteredOptions = new ArrayList<>();
-        getPredefinedDeviceProfiles(context, gridName, filteredOptions, allOptions);
-
-        if (allOptions.isEmpty() && filteredOptions.isEmpty()) {
-            throw new RuntimeException("No display option with canBeDefault=true");
-        }
-
-        // Sort the profiles based on the closeness to the device size
-        Comparator<DisplayOption> comparator = (a, b) -> Float.compare(dist(minWidthDps,
-                minHeightDps, a.minWidthDps, a.minHeightDps),
-                dist(minWidthDps, minHeightDps, b.minWidthDps, b.minHeightDps));
-
-        // Calculate the device profiles as if there is no grid override.
-        Collections.sort(allOptions, comparator);
-        DisplayOption interpolatedDisplayOption =
-                invDistWeightedInterpolate(minWidthDps,  minHeightDps, allOptions);
-        initGridOption(context, allOptions, interpolatedDisplayOption, displayInfo.metrics);
-
-        // Create IDP with no grid override values.
-        InvariantDeviceProfile originalIDP = new InvariantDeviceProfile(this);
-        originalIDP.landscapeProfile = new DeviceProfile(context, this, null, smallestSize,
-                largestSize, largeSide, smallSide, true /* isLandscape */,
-                false /* isMultiWindowMode */);
-        originalIDP.portraitProfile = new DeviceProfile(context, this, null, smallestSize,
-                largestSize, smallSide, largeSide, false /* isLandscape */,
-                false /* isMultiWindowMode */);
-
-        if (filteredOptions.isEmpty()) {
-            filteredOptions = allOptions;
-
-            landscapeProfile = originalIDP.landscapeProfile;
-            portraitProfile = originalIDP.portraitProfile;
-        } else {
-            Collections.sort(filteredOptions, comparator);
-            interpolatedDisplayOption =
-                    invDistWeightedInterpolate(minWidthDps, minHeightDps, filteredOptions);
-
-            initGridOption(context, filteredOptions, interpolatedDisplayOption,
-                    displayInfo.metrics);
-            numAllAppsColumns = originalIDP.numAllAppsColumns;
-
-            landscapeProfile = new DeviceProfile(context, this, originalIDP, smallestSize,
-                    largestSize, largeSide, smallSide, true /* isLandscape */,
-                    false /* isMultiWindowMode */);
-            portraitProfile = new DeviceProfile(context, this, originalIDP, smallestSize,
-                    largestSize, smallSide, largeSide, false /* isLandscape */,
-                    false /* isMultiWindowMode */);
-        }
-
-        GridOption closestProfile = filteredOptions.get(0).grid;
-        if (!closestProfile.name.equals(gridName)) {
-            Utilities.getPrefs(context).edit()
-                    .putString(KEY_IDP_GRID_NAME, closestProfile.name).apply();
-        }
+        landscapeProfile = new DeviceProfile(context, this, smallestSize, largestSize,
+                largeSide, smallSide, true /* isLandscape */, false /* isMultiWindowMode */);
+        portraitProfile = new DeviceProfile(context, this, smallestSize, largestSize,
+                smallSide, largeSide, false /* isLandscape */, false /* isMultiWindowMode */);
 
         // We need to ensure that there is enough extra space in the wallpaper
         // for the intended parallax effects
@@ -298,34 +287,6 @@
         return closestProfile.name;
     }
 
-    private void initGridOption(Context context, ArrayList<DisplayOption> options,
-            DisplayOption displayOption, DisplayMetrics metrics) {
-        GridOption closestProfile = options.get(0).grid;
-        numRows = closestProfile.numRows;
-        numColumns = closestProfile.numColumns;
-        numHotseatIcons = closestProfile.numHotseatIcons;
-        dbFile = closestProfile.dbFile;
-        defaultLayoutId = closestProfile.defaultLayoutId;
-        demoModeLayoutId = closestProfile.demoModeLayoutId;
-        numFolderRows = closestProfile.numFolderRows;
-        numFolderColumns = closestProfile.numFolderColumns;
-        numAllAppsColumns = numColumns;
-
-        mExtraAttrs = closestProfile.extraAttrs;
-
-        iconSize = displayOption.iconSize;
-        iconShapePath = getIconShapePath(context);
-        landscapeIconSize = displayOption.landscapeIconSize;
-        iconBitmapSize = ResourceUtils.pxFromDp(iconSize, metrics);
-        iconTextSize = displayOption.iconTextSize;
-        fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
-
-        // If the partner customization apk contains any grid overrides, apply them
-        // Supported overrides: numRows, numColumns, iconSize
-        applyPartnerDeviceProfileOverrides(context, metrics);
-    }
-
-
     @Nullable
     public TypedValue getAttrValue(int attr) {
         return mExtraAttrs == null ? null : mExtraAttrs.get(attr);
@@ -403,13 +364,7 @@
         }
     }
 
-    /**
-     * @param gridName The current grid name.
-     * @param filteredOptionsOut List filled with all the filtered options based on gridName.
-     * @param allOptionsOut List filled with all the options that can be the default option.
-     */
-    static void getPredefinedDeviceProfiles(Context context, String gridName,
-            ArrayList<DisplayOption> filteredOptionsOut, ArrayList<DisplayOption> allOptionsOut) {
+    static ArrayList<DisplayOption> getPredefinedDeviceProfiles(Context context, String gridName) {
         ArrayList<DisplayOption> profiles = new ArrayList<>();
         try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
             final int depth = parser.getDepth();
@@ -436,19 +391,26 @@
             throw new RuntimeException(e);
         }
 
+        ArrayList<DisplayOption> filteredProfiles = new ArrayList<>();
         if (!TextUtils.isEmpty(gridName)) {
             for (DisplayOption option : profiles) {
                 if (gridName.equals(option.grid.name)) {
-                    filteredOptionsOut.add(option);
+                    filteredProfiles.add(option);
                 }
             }
         }
-
-        for (DisplayOption option : profiles) {
-            if (option.canBeDefault) {
-                allOptionsOut.add(option);
+        if (filteredProfiles.isEmpty()) {
+            // No grid found, use the default options
+            for (DisplayOption option : profiles) {
+                if (option.canBeDefault) {
+                    filteredProfiles.add(option);
+                }
             }
         }
+        if (filteredProfiles.isEmpty()) {
+            throw new RuntimeException("No display option with canBeDefault=true");
+        }
+        return filteredProfiles;
     }
 
     private int getLauncherIconDensity(int requiredSize) {
@@ -493,7 +455,7 @@
 
     @VisibleForTesting
     static DisplayOption invDistWeightedInterpolate(float width, float height,
-                ArrayList<DisplayOption> points) {
+            ArrayList<DisplayOption> points) {
         float weights = 0;
 
         DisplayOption p = points.get(0);
@@ -573,6 +535,8 @@
         private final int numHotseatIcons;
 
         private final String dbFile;
+        private final int numAllAppsColumns;
+
         private final int defaultLayoutId;
         private final int demoModeLayoutId;
 
@@ -596,6 +560,8 @@
                     R.styleable.GridDisplayOption_numFolderRows, numRows);
             numFolderColumns = a.getInt(
                     R.styleable.GridDisplayOption_numFolderColumns, numColumns);
+            numAllAppsColumns = a.getInt(
+                    R.styleable.GridDisplayOption_numAllAppsColumns, numColumns);
 
             a.recycle();
 
@@ -615,6 +581,8 @@
         private float iconSize;
         private float iconTextSize;
         private float landscapeIconSize;
+        private float allAppsIconSize;
+        private float allAppsIconTextSize;
 
         DisplayOption(GridOption grid, Context context, AttributeSet attrs) {
             this.grid = grid;
@@ -633,6 +601,10 @@
                     iconSize);
             iconTextSize = a.getFloat(R.styleable.ProfileDisplayOption_iconTextSize, 0);
 
+            allAppsIconSize = a.getFloat(R.styleable.ProfileDisplayOption_allAppsIconSize,
+                    iconSize);
+            allAppsIconTextSize = a.getFloat(R.styleable.ProfileDisplayOption_allAppsIconTextSize,
+                    iconTextSize);
             a.recycle();
         }
 
@@ -647,14 +619,18 @@
         private DisplayOption multiply(float w) {
             iconSize *= w;
             landscapeIconSize *= w;
+            allAppsIconSize *= w;
             iconTextSize *= w;
+            allAppsIconTextSize *= w;
             return this;
         }
 
         private DisplayOption add(DisplayOption p) {
             iconSize += p.iconSize;
             landscapeIconSize += p.landscapeIconSize;
+            allAppsIconSize += p.allAppsIconSize;
             iconTextSize += p.iconTextSize;
+            allAppsIconTextSize += p.allAppsIconTextSize;
             return this;
         }
     }
@@ -672,4 +648,4 @@
             onConfigChanged(context);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 48db5ea..f69940b 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -139,6 +139,7 @@
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.PendingRequestArgs;
@@ -300,6 +301,7 @@
     // We only want to get the SharedPreferences once since it does an FS stat each time we get
     // it from the context.
     private SharedPreferences mSharedPrefs;
+    private OnboardingPrefs mOnboardingPrefs;
 
     // Activity result which needs to be processed after workspace has loaded.
     private ActivityResultInfo mPendingActivityResult;
@@ -367,6 +369,8 @@
         mAllAppsController = new AllAppsTransitionController(this);
         mStateManager = new LauncherStateManager(this);
 
+        mOnboardingPrefs = createOnboardingPrefs(mSharedPrefs, mStateManager);
+
         mAppWidgetManager = new WidgetManagerHelper(this);
         mAppWidgetHost = new LauncherAppWidgetHost(this,
                 appWidgetId -> getWorkspace().removeWidget(appWidgetId));
@@ -458,6 +462,15 @@
         return new LauncherOverlayManager() { };
     }
 
+    protected OnboardingPrefs createOnboardingPrefs(SharedPreferences sharedPrefs,
+            LauncherStateManager stateManager) {
+        return new OnboardingPrefs<>(this, sharedPrefs, stateManager);
+    }
+
+    public OnboardingPrefs getOnboardingPrefs() {
+        return mOnboardingPrefs;
+    }
+
     @Override
     public void onPluginConnected(OverlayPlugin overlayManager, Context context) {
         switchOverlay(() -> overlayManager.createOverlayManager(this, this));
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 69faa49..a45c96c 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -645,8 +645,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);
+            recyclerView.setContentDescription(workDisabled ? mLauncher.getString(
+                    R.string.work_apps_paused_content_description) : null);
             View overlayView = getOverlayView();
             recyclerView.setItemAnimator(new DefaultItemAnimator());
             if (workDisabled) {
diff --git a/src/com/android/launcher3/allapps/DiscoveryBounce.java b/src/com/android/launcher3/allapps/DiscoveryBounce.java
index 0f0fc3a..fc29a30 100644
--- a/src/com/android/launcher3/allapps/DiscoveryBounce.java
+++ b/src/com/android/launcher3/allapps/DiscoveryBounce.java
@@ -24,7 +24,6 @@
 import android.animation.Animator;
 import android.animation.AnimatorInflater;
 import android.animation.AnimatorListenerAdapter;
-import android.content.SharedPreferences;
 import android.os.Handler;
 import android.os.UserManager;
 import android.view.MotionEvent;
@@ -35,6 +34,7 @@
 import com.android.launcher3.LauncherStateManager.StateListener;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.util.OnboardingPrefs;
 
 /**
  * Abstract base class of floating view responsible for showing discovery bounce animation
@@ -43,13 +43,6 @@
 
     private static final long DELAY_MS = 450;
 
-    public static final String HOME_BOUNCE_SEEN = "launcher.apps_view_shown";
-    public static final String SHELF_BOUNCE_SEEN = "launcher.shelf_bounce_seen";
-    public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_count";
-    public static final String SHELF_BOUNCE_COUNT = "launcher.shelf_bounce_count";
-
-    public static final int BOUNCE_MAX_COUNT = 3;
-
     private final Launcher mLauncher;
     private final Animator mDiscoBounceAnimation;
 
@@ -142,8 +135,9 @@
     }
 
     private static void showForHomeIfNeeded(Launcher launcher, boolean withDelay) {
+        OnboardingPrefs onboardingPrefs = launcher.getOnboardingPrefs();
         if (!launcher.isInState(NORMAL)
-                || launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)
+                || onboardingPrefs.getBoolean(OnboardingPrefs.HOME_BOUNCE_SEEN)
                 || AbstractFloatingView.getTopOpenView(launcher) != null
                 || launcher.getSystemService(UserManager.class).isDemoUser()
                 || Utilities.IS_RUNNING_IN_TEST_HARNESS) {
@@ -154,7 +148,7 @@
             new Handler().postDelayed(() -> showForHomeIfNeeded(launcher, false), DELAY_MS);
             return;
         }
-        incrementHomeBounceCount(launcher);
+        onboardingPrefs.incrementEventCount(OnboardingPrefs.HOME_BOUNCE_COUNT);
 
         new DiscoveryBounce(launcher, 0).show(HOTSEAT);
     }
@@ -164,11 +158,12 @@
     }
 
     private static void showForOverviewIfNeeded(Launcher launcher, boolean withDelay) {
+        OnboardingPrefs onboardingPrefs = launcher.getOnboardingPrefs();
         if (!launcher.isInState(OVERVIEW)
                 || !launcher.hasBeenResumed()
                 || launcher.isForceInvisible()
                 || launcher.getDeviceProfile().isVerticalBarLayout()
-                || launcher.getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false)
+                || onboardingPrefs.getBoolean(OnboardingPrefs.SHELF_BOUNCE_SEEN)
                 || launcher.getSystemService(UserManager.class).isDemoUser()
                 || Utilities.IS_RUNNING_IN_TEST_HARNESS) {
             return;
@@ -182,7 +177,7 @@
             // TODO: Move these checks to the top and call this method after invalidate handler.
             return;
         }
-        incrementShelfBounceCount(launcher);
+        onboardingPrefs.incrementEventCount(OnboardingPrefs.SHELF_BOUNCE_COUNT);
 
         new DiscoveryBounce(launcher, (1 - OVERVIEW.getVerticalProgress(launcher)))
                 .show(PREDICTION);
@@ -209,22 +204,4 @@
             mController.setProgress(progress - mDelta);
         }
     }
-
-    private static void incrementShelfBounceCount(Launcher launcher) {
-        SharedPreferences sharedPrefs = launcher.getSharedPrefs();
-        int count = sharedPrefs.getInt(SHELF_BOUNCE_COUNT, 0);
-        if (count > BOUNCE_MAX_COUNT) {
-            return;
-        }
-        sharedPrefs.edit().putInt(SHELF_BOUNCE_COUNT, count + 1).apply();
-    }
-
-    private static void incrementHomeBounceCount(Launcher launcher) {
-        SharedPreferences sharedPrefs = launcher.getSharedPrefs();
-        int count = sharedPrefs.getInt(HOME_BOUNCE_COUNT, 0);
-        if (count > BOUNCE_MAX_COUNT) {
-            return;
-        }
-        sharedPrefs.edit().putInt(HOME_BOUNCE_COUNT, count + 1).apply();
-    }
 }
diff --git a/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java b/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java
index 2515c24..2de425e 100644
--- a/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java
+++ b/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java
@@ -135,8 +135,8 @@
     @Override
     public void setActiveMarker(int activePage) {
         updateTabTextColor(activePage);
-        updateIndicatorPosition(activePage);
         if (mContainerView != null && mLastActivePage != activePage) {
+            updateIndicatorPosition(activePage);
             mContainerView.onTabChanged(activePage);
         }
         mLastActivePage = activePage;
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
index 05db18e..33262b6 100644
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java
@@ -25,6 +25,8 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
 import android.widget.Switch;
 
 import com.android.launcher3.Insettable;
@@ -46,16 +48,28 @@
 
     private Rect mInsets = new Rect();
 
+    private final float[] mTouch = new float[2];
+    private int mTouchSlop;
+
     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() {
+        ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
+        mTouchSlop = viewConfiguration.getScaledTouchSlop();
     }
 
     @Override
@@ -73,7 +87,7 @@
 
     private void setCheckedInternal(boolean checked) {
         super.setChecked(checked);
-        setCompoundDrawablesWithIntrinsicBounds(
+        setCompoundDrawablesRelativeWithIntrinsicBounds(
                 checked ? R.drawable.ic_corp : R.drawable.ic_corp_off, 0, 0, 0);
     }
 
@@ -84,6 +98,25 @@
         setEnabled(true);
     }
 
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            mTouch[0] = ev.getX();
+            mTouch[1] = ev.getY();
+        } else if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
+            if (Math.abs(mTouch[0] - ev.getX()) > mTouchSlop
+                    || Math.abs(mTouch[1] - ev.getY()) > mTouchSlop) {
+                int action = ev.getAction();
+                ev.setAction(MotionEvent.ACTION_CANCEL);
+                super.onTouchEvent(ev);
+                ev.setAction(action);
+                return false;
+            }
+        }
+        return super.onTouchEvent(ev);
+    }
+
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         super.onLayout(changed, l, t, r, b);
diff --git a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
index 9b91a1d..e47a16f 100644
--- a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
@@ -92,7 +92,7 @@
             postCleanup();
             return false;
         }
-        if (event.getAction() == DragEvent.ACTION_DRAG_STARTED) {
+        if (event.getAction() == DragEvent.ACTION_DRAG_STARTED || !mDragController.isDragging()) {
             if (onDragStart(event)) {
                 return true;
             } else {
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 96ddd57..32531c0 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -153,6 +153,7 @@
             CellLayout page = (CellLayout) getChildAt(i);
             ShortcutAndWidgetContainer container = page.getShortcutsAndWidgets();
             for (int j = container.getChildCount() - 1; j >= 0; j--) {
+                container.getChildAt(j).setVisibility(View.VISIBLE);
                 mViewCache.recycleView(R.layout.folder_application, container.getChildAt(j));
             }
             page.removeAllViews();
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 015de59..e7449bb 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -97,7 +97,6 @@
 
     public static final String PERMANENT_DIAG_TAG = "TaplTarget";
 
-    public static final String NO_BACKGROUND_TO_OVERVIEW_TAG = "b/138251824";
     public static final String APP_NOT_DISABLED = "b/139891609";
     public static final String NO_SCROLL_END_WIDGETS = "b/152354290";
 }
diff --git a/src/com/android/launcher3/util/OnboardingPrefs.java b/src/com/android/launcher3/util/OnboardingPrefs.java
new file mode 100644
index 0000000..baa1eee
--- /dev/null
+++ b/src/com/android/launcher3/util/OnboardingPrefs.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import android.content.SharedPreferences;
+import android.util.ArrayMap;
+
+import androidx.annotation.StringDef;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherStateManager;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Stores and retrieves onboarding-related data via SharedPreferences.
+ */
+public class OnboardingPrefs<T extends Launcher> {
+
+    public static final String HOME_BOUNCE_SEEN = "launcher.apps_view_shown";
+    public static final String SHELF_BOUNCE_SEEN = "launcher.shelf_bounce_seen";
+    public static final String HOME_BOUNCE_COUNT = "launcher.home_bounce_count";
+    public static final String SHELF_BOUNCE_COUNT = "launcher.shelf_bounce_count";
+    public static final String ALL_APPS_COUNT = "launcher.all_apps_count";
+
+    /**
+     * Events that either have happened or have not (booleans).
+     */
+    @StringDef(value = {
+            HOME_BOUNCE_SEEN,
+            SHELF_BOUNCE_SEEN,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EventBoolKey {}
+
+    /**
+     * Events that occur multiple times, which we count up to a max defined in {@link #MAX_COUNTS}.
+     */
+    @StringDef(value = {
+            HOME_BOUNCE_COUNT,
+            SHELF_BOUNCE_COUNT,
+            ALL_APPS_COUNT,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EventCountKey {}
+
+    private static final Map<String, Integer> MAX_COUNTS;
+    static {
+        Map<String, Integer> maxCounts = new ArrayMap<>(3);
+        maxCounts.put(HOME_BOUNCE_COUNT, 3);
+        maxCounts.put(SHELF_BOUNCE_COUNT, 3);
+        maxCounts.put(ALL_APPS_COUNT, 5);
+        MAX_COUNTS = Collections.unmodifiableMap(maxCounts);
+    }
+
+    protected final T mLauncher;
+    protected final SharedPreferences mSharedPrefs;
+    protected final LauncherStateManager mStateManager;
+
+    public OnboardingPrefs(T launcher, SharedPreferences sharedPrefs,
+            LauncherStateManager stateManager) {
+        mLauncher = launcher;
+        mSharedPrefs = sharedPrefs;
+        mStateManager = stateManager;
+    }
+
+    /** @return The number of times we have seen the given event. */
+    public int getCount(@EventCountKey String key) {
+        return mSharedPrefs.getInt(key, 0);
+    }
+
+    /** @return Whether we have seen this event enough times, as defined by {@link #MAX_COUNTS}. */
+    public boolean hasReachedMaxCount(@EventCountKey String eventKey) {
+        return hasReachedMaxCount(getCount(eventKey), eventKey);
+    }
+
+    private boolean hasReachedMaxCount(int count, @EventCountKey String eventKey) {
+        return count >= MAX_COUNTS.get(eventKey);
+    }
+
+    /** @return Whether we have seen the given event. */
+    public boolean getBoolean(@EventBoolKey String key) {
+        return mSharedPrefs.getBoolean(key, false);
+    }
+
+    /**
+     * Add 1 to the given event count, if we haven't already reached the max count.
+     * @return Whether we have now reached the max count.
+     */
+    public boolean incrementEventCount(@EventCountKey String eventKey) {
+        int count = getCount(eventKey);
+        if (hasReachedMaxCount(count, eventKey)) {
+            return true;
+        }
+        count++;
+        mSharedPrefs.edit().putInt(eventKey, count).apply();
+        return hasReachedMaxCount(count, eventKey);
+    }
+}
diff --git a/src/com/android/launcher3/views/ScrimView.java b/src/com/android/launcher3/views/ScrimView.java
index 39e1eac..442c5fd 100644
--- a/src/com/android/launcher3/views/ScrimView.java
+++ b/src/com/android/launcher3/views/ScrimView.java
@@ -82,7 +82,7 @@
  * Simple scrim which draws a flat color
  */
 public class ScrimView<T extends Launcher> extends View implements Insettable, OnChangeListener,
-        AccessibilityStateChangeListener, StateListener {
+        AccessibilityStateChangeListener {
 
     public static final IntProperty<ScrimView> DRAG_HANDLE_ALPHA =
             new IntProperty<ScrimView>("dragHandleAlpha") {
@@ -116,6 +116,18 @@
     private final AccessibilityManager mAM;
     protected final int mEndScrim;
 
+    private final StateListener mAccessibilityLauncherStateListener = new StateListener() {
+        @Override
+        public void onStateTransitionStart(LauncherState toState) {}
+
+        @Override
+        public void onStateTransitionComplete(LauncherState finalState) {
+            setImportantForAccessibility(finalState == ALL_APPS
+                    ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                    : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+        }
+    };
+
     protected float mMaxScrimAlpha;
 
     protected float mProgress = 1;
@@ -177,7 +189,7 @@
     @Override
     public void setInsets(Rect insets) {
         updateDragHandleBounds();
-        updateDragHandleVisibility(null);
+        updateDragHandleVisibility();
     }
 
     @Override
@@ -375,18 +387,22 @@
     @Override
     public void onAccessibilityStateChanged(boolean enabled) {
         LauncherStateManager stateManager = mLauncher.getStateManager();
-        stateManager.removeStateListener(this);
+        stateManager.removeStateListener(mAccessibilityLauncherStateListener);
 
         if (enabled) {
-            stateManager.addStateListener(this);
-            handleStateChangedComplete(stateManager.getState());
+            stateManager.addStateListener(mAccessibilityLauncherStateListener);
+            mAccessibilityLauncherStateListener.onStateTransitionComplete(stateManager.getState());
         } else {
             setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
         }
+        updateDragHandleVisibility();
+    }
+
+    public void updateDragHandleVisibility() {
         updateDragHandleVisibility(null);
     }
 
-    private void updateDragHandleVisibility(Drawable recycle) {
+    private void updateDragHandleVisibility(@Nullable Drawable recycle) {
         boolean visible = shouldDragHandleBeVisible();
         boolean wasVisible = mDragHandle != null;
         if (visible != wasVisible) {
@@ -424,20 +440,6 @@
         mAccessibilityHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
     }
 
-    @Override
-    public void onStateTransitionStart(LauncherState toState) {}
-
-    @Override
-    public void onStateTransitionComplete(LauncherState finalState) {
-        handleStateChangedComplete(finalState);
-    }
-
-    private void handleStateChangedComplete(LauncherState finalState) {
-        setImportantForAccessibility(finalState == ALL_APPS
-                ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-    }
-
     protected class AccessibilityHelper extends ExploreByTouchHelper {
 
         private static final int DRAG_HANDLE_ID = 1;
diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
index 63e063f..17baa27 100644
--- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
@@ -22,7 +22,6 @@
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.accessibility.AccessibilityNodeInfo;
 
 import com.android.launcher3.BaseRecyclerView;
 import com.android.launcher3.R;
@@ -210,24 +209,4 @@
         }
         super.stopNestedScroll();
     }
-
-    @Override
-    public void setLayoutFrozen(boolean frozen) {
-        if (frozen != isLayoutSuppressed()) {
-            if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-                Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "setLayoutFrozen " + frozen
-                        + " @ " + android.util.Log.getStackTraceString(new Throwable()));
-            }
-        }
-        super.setLayoutFrozen(frozen);
-    }
-
-    @Override
-    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
-        super.onInitializeAccessibilityNodeInfo(info);
-        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS,
-                    "onInitializeAccessibilityNodeInfo, scrollable: " + info.isScrollable());
-        }
-    }
 }
\ No newline at end of file
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java b/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java
index a56801f..313ea05 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -15,8 +15,8 @@
  */
 package com.android.launcher3.uioverrides.states;
 
-import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_SEEN;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 710ce9e..84eae2d 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -247,6 +247,8 @@
                 }
             }
         }
+
+        disableSensorRotation();
     }
 
     public void enableCheckEventsForSuccessfulGestures() {