Merge "Recognizing running "prebuilt" launcher and manually built platform" into ub-launcher3-master
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index f3db20e..9123959 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,2 +1,2 @@
 [Hook Scripts]
-checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --config_xml tools/checkstyle.xml --sha ${PREUPLOAD_COMMIT}
diff --git a/go/src/com/android/launcher3/model/LoaderResults.java b/go/src/com/android/launcher3/model/LoaderResults.java
index 26c3313..7130531 100644
--- a/go/src/com/android/launcher3/model/LoaderResults.java
+++ b/go/src/com/android/launcher3/model/LoaderResults.java
@@ -16,10 +16,11 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.model.BgDataModel.Callbacks;
-
-import java.lang.ref.WeakReference;
+import com.android.launcher3.util.LooperExecutor;
 
 /**
  * Helper class to handle results of {@link com.android.launcher3.model.LoaderTask}.
@@ -27,8 +28,13 @@
 public class LoaderResults extends BaseLoaderResults {
 
     public LoaderResults(LauncherAppState app, BgDataModel dataModel,
-            AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
-        super(app, dataModel, allAppsList, pageToBindFirst, callbacks);
+            AllAppsList allAppsList, Callbacks[] callbacks) {
+        this(app, dataModel, allAppsList, callbacks, MAIN_EXECUTOR);
+    }
+
+    public LoaderResults(LauncherAppState app, BgDataModel dataModel,
+            AllAppsList allAppsList, Callbacks[] callbacks, LooperExecutor executor) {
+        super(app, dataModel, allAppsList, callbacks, executor);
     }
 
     @Override
diff --git a/go/src/com/android/launcher3/model/WidgetsModel.java b/go/src/com/android/launcher3/model/WidgetsModel.java
index 7b8f4e6..7269b41 100644
--- a/go/src/com/android/launcher3/model/WidgetsModel.java
+++ b/go/src/com/android/launcher3/model/WidgetsModel.java
@@ -19,8 +19,10 @@
 import android.content.Context;
 import android.os.UserHandle;
 
-import com.android.launcher3.icons.ComponentWithLabel;
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.icons.ComponentWithLabel;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.WidgetListRowEntry;
 
@@ -29,8 +31,6 @@
 import java.util.List;
 import java.util.Set;
 
-import androidx.annotation.Nullable;
-
 /**
  * Widgets data model that is used by the adapters of the widget views and controllers.
  *
@@ -39,7 +39,7 @@
 public class WidgetsModel {
 
     // True is the widget support is disabled.
-    public static final boolean GO_DISABLE_WIDGETS = false;
+    public static final boolean GO_DISABLE_WIDGETS = true;
 
     private static final ArrayList<WidgetListRowEntry> EMPTY_WIDGET_LIST = new ArrayList<>();
 
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 826a275..5d871c3 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -91,6 +91,17 @@
                   android:taskAffinity="${packageName}.locktask"
                   android:directBootAware="true" />
 
+        <activity
+            android:name="com.android.quickstep.interaction.BackGestureTutorialActivity"
+            android:autoRemoveFromRecents="true"
+            android:excludeFromRecents="true"
+            android:screenOrientation="portrait">
+            <intent-filter>
+                <action android:name="com.android.quickstep.action.BACK_GESTURE_TUTORIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
     </application>
 
 </manifest>
diff --git a/quickstep/recents_ui_overrides/res/drawable/hotseat_edu_notification_icon.xml b/quickstep/recents_ui_overrides/res/drawable/hotseat_edu_notification_icon.xml
new file mode 100644
index 0000000..f0e70a8
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/drawable/hotseat_edu_notification_icon.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector android:height="24dp" android:viewportHeight="24"
+    android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@color/hotseat_edu_background" android:pathData="M19 9l1.25-2.75L23 5l-2.75-1.25L19 1l-1.25 2.75L15 5l2.75 1.25L19 9zm-7.5.5L9 4 6.5 9.5 1 12l5.5 2.5L9 20l2.5-5.5L17 12l-5.5-2.5zM19 15l-1.25 2.75L15 19l2.75 1.25L19 23l1.25-2.75L23 19l-2.75-1.25L19 15z"/>
+</vector>
diff --git a/quickstep/recents_ui_overrides/res/drawable/hotseat_prediction_edu_top.xml b/quickstep/recents_ui_overrides/res/drawable/hotseat_prediction_edu_top.xml
new file mode 100644
index 0000000..e3cc549
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/drawable/hotseat_prediction_edu_top.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector android:height="15.53398dp" android:viewportHeight="32"
+    android:viewportWidth="412" android:width="200dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@color/hotseat_edu_background" android:pathData="M412,32v-2.64C349.26,10.51 279.5,0 206,0S62.74,10.51 0,29.36V32H412z"/>
+</vector>
diff --git a/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml b/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml
new file mode 100644
index 0000000..ee38e3b
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/layout/predicted_hotseat_edu.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.launcher3.hybridhotseat.HotseatEduDialog xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_gravity="bottom"
+    android:layout_height="wrap_content"
+    android:gravity="bottom"
+    android:orientation="vertical">
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="32dp"
+        android:background="@drawable/hotseat_prediction_edu_top" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="@color/hotseat_edu_background"
+        android:orientation="vertical">
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="18dp"
+            android:fontFamily="google-sans"
+            android:paddingLeft="@dimen/hotseat_edu_padding"
+            android:paddingRight="@dimen/hotseat_edu_padding"
+            android:text="@string/hotseat_migrate_title"
+            android:textAlignment="center"
+            android:textColor="@android:color/white"
+            android:textSize="20sp" />
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="18dp"
+            android:layout_marginBottom="18dp"
+            android:fontFamily="roboto-medium"
+            android:paddingLeft="@dimen/hotseat_edu_padding"
+            android:paddingRight="@dimen/hotseat_edu_padding"
+            android:text="@string/hotseat_migrate_message"
+            android:textAlignment="center"
+            android:textColor="@android:color/white"
+            android:textSize="16sp" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:id="@+id/hotseat_wrapper"
+            android:orientation="vertical">
+
+            <com.android.launcher3.CellLayout
+                android:id="@+id/sample_prediction"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                launcher:containerType="hotseat" />
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingLeft="@dimen/hotseat_edu_padding"
+                android:paddingTop="8dp"
+                android:paddingRight="@dimen/hotseat_edu_padding">
+
+                <Button
+                    android:id="@+id/turn_predictions_on"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="end"
+                    android:background="?android:attr/selectableItemBackground"
+                    android:text="@string/hotseat_migrate_accept"
+                    android:textAlignment="textEnd"
+                    android:textColor="@android:color/white" />
+
+                <Button
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:id="@+id/no_thanks"
+                    android:text="@string/hotseat_migrate_dismiss"
+                    android:layout_gravity="start"
+                    android:background="?android:attr/selectableItemBackground"
+                    android:textColor="@android:color/white" />
+
+            </FrameLayout>
+        </LinearLayout>
+    </LinearLayout>
+
+</com.android.launcher3.hybridhotseat.HotseatEduDialog>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/colors.xml b/quickstep/recents_ui_overrides/res/values/colors.xml
index 7426e30..4fa5684 100644
--- a/quickstep/recents_ui_overrides/res/values/colors.xml
+++ b/quickstep/recents_ui_overrides/res/values/colors.xml
@@ -6,4 +6,6 @@
     <color name="all_apps_label_text_dark">#61FFFFFF</color>
     <color name="all_apps_prediction_row_separator">#3c000000</color>
     <color name="all_apps_prediction_row_separator_dark">#3cffffff</color>
+
+    <color name="hotseat_edu_background">#f01A73E8</color>
 </resources>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/res/values/dimens.xml b/quickstep/recents_ui_overrides/res/values/dimens.xml
index ee672d4..c458ec7 100644
--- a/quickstep/recents_ui_overrides/res/values/dimens.xml
+++ b/quickstep/recents_ui_overrides/res/values/dimens.xml
@@ -29,8 +29,7 @@
     <dimen name="swipe_up_y_overshoot">10dp</dimen>
     <dimen name="swipe_up_max_workspace_trans_y">-60dp</dimen>
 
-    <!-- Predicted icon related -->
-    <dimen name="predicted_icon_background_corner_radius">15dp</dimen>
-    <dimen name="predicted_icon_background_inset">8dp</dimen>
+    <!-- Hybrid hotseat related -->
+    <dimen name="hotseat_edu_padding">24dp</dimen>
 
 </resources>
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
index e45eded..06b9f1f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
@@ -26,7 +26,6 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.launcher3.HotseatPredictionController;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener;
 import com.android.launcher3.ItemInfo;
@@ -39,6 +38,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsStore.OnUpdateListener;
+import com.android.launcher3.hybridhotseat.HotseatPredictionController;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
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
new file mode 100644
index 0000000..923e050
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduController.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.hybridhotseat;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.core.app.NotificationCompat;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+import com.android.launcher3.util.ActivityTracker;
+
+import java.util.List;
+
+/**
+ * Controller class for managing user onboaridng flow for hybrid hotseat
+ */
+public class HotseatEduController {
+    public static final String KEY_HOTSEAT_EDU_SEEN = "hotseat_edu_seen";
+
+    private static final String NOTIFICATION_CHANNEL_ID = "launcher_onboarding";
+    private static final int ONBOARDING_NOTIFICATION_ID = 7641;
+
+    private final Launcher mLauncher;
+    private List<WorkspaceItemInfo> mPredictedApps;
+    private HotseatEduDialog mActiveDialog;
+
+    private final NotificationManager mNotificationManager;
+    private final Notification mNotification;
+
+    HotseatEduController(Launcher launcher) {
+        mLauncher = launcher;
+        mNotificationManager = mLauncher.getSystemService(NotificationManager.class);
+        createNotificationChannel();
+        mNotification = createNotification();
+    }
+
+    void migrate() {
+        ViewGroup hotseatVG = mLauncher.getHotseat().getShortcutsAndWidgets();
+        int workspacePageCount = mLauncher.getWorkspace().getPageCount();
+        for (int i = 0; i < hotseatVG.getChildCount(); i++) {
+            View child = hotseatVG.getChildAt(i);
+            ItemInfo tag = (ItemInfo) child.getTag();
+            mLauncher.getModelWriter().moveItemInDatabase(tag,
+                    LauncherSettings.Favorites.CONTAINER_DESKTOP, workspacePageCount, tag.screenId,
+                    0);
+        }
+    }
+
+    void removeNotification() {
+        mNotificationManager.cancel(ONBOARDING_NOTIFICATION_ID);
+    }
+
+    void finishOnboarding() {
+        mLauncher.getModel().rebindCallbacks();
+        mLauncher.getSharedPrefs().edit().putBoolean(KEY_HOTSEAT_EDU_SEEN, true).apply();
+        removeNotification();
+    }
+
+    void setPredictedApps(List<WorkspaceItemInfo> predictedApps) {
+        mPredictedApps = predictedApps;
+        if (!mPredictedApps.isEmpty()
+                && mLauncher.getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
+            mNotificationManager.notify(ONBOARDING_NOTIFICATION_ID, mNotification);
+        }
+    }
+
+    private void createNotificationChannel() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
+        CharSequence name = mLauncher.getString(R.string.hotseat_migrate_title);
+        int importance = NotificationManager.IMPORTANCE_LOW;
+        NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, name,
+                importance);
+        mNotificationManager.createNotificationChannel(channel);
+    }
+
+    private Notification createNotification() {
+        Intent intent = new Intent(mLauncher.getApplicationContext(), mLauncher.getClass());
+        intent = new NotificationHandler().addToIntent(intent);
+
+        CharSequence name = mLauncher.getString(R.string.hotseat_migrate_prompt_title);
+        String description = mLauncher.getString(R.string.hotseat_migrate_prompt_content);
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(mLauncher,
+                NOTIFICATION_CHANNEL_ID)
+                .setContentTitle(name)
+                .setOngoing(true)
+                .setColor(mLauncher.getColor(R.color.hotseat_edu_background))
+                .setContentIntent(PendingIntent.getActivity(mLauncher, 0, intent,
+                        PendingIntent.FLAG_CANCEL_CURRENT))
+                .setSmallIcon(R.drawable.hotseat_edu_notification_icon)
+                .setContentText(description);
+        return builder.build();
+
+    }
+
+    void destroy() {
+        removeNotification();
+        if (mActiveDialog != null) {
+            mActiveDialog.setHotseatEduController(null);
+        }
+    }
+
+    void showDialog() {
+        if (mPredictedApps == null || mPredictedApps.isEmpty()) {
+            return;
+        }
+        if (mActiveDialog != null) {
+            mActiveDialog.handleClose(false);
+        }
+        mActiveDialog = HotseatEduDialog.getDialog(mLauncher);
+        mActiveDialog.setHotseatEduController(this);
+        mActiveDialog.show(mPredictedApps);
+    }
+
+    static class NotificationHandler implements
+            ActivityTracker.SchedulerCallback<QuickstepLauncher> {
+        @Override
+        public boolean init(QuickstepLauncher activity, boolean alreadyOnHome) {
+            activity.getHotseatPredictionController().showEduDialog();
+            return true;
+        }
+    }
+}
+
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
new file mode 100644
index 0000000..4c87945
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.hybridhotseat;
+
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.Toast;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.uioverrides.PredictedAppIcon;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.views.AbstractSlideInView;
+
+import java.util.List;
+
+/**
+ * User education dialog for hybrid hotseat. Allows user to migrate hotseat items to a new page in
+ * the workspace and shows predictions on the whole hotseat
+ */
+public class HotseatEduDialog extends AbstractSlideInView implements Insettable {
+
+    private static final int DEFAULT_CLOSE_DURATION = 200;
+
+    public static boolean shown = false;
+
+    private final Rect mInsets = new Rect();
+    private View mHotseatWrapper;
+    private CellLayout mSampleHotseat;
+
+    public void setHotseatEduController(HotseatEduController hotseatEduController) {
+        mHotseatEduController = hotseatEduController;
+    }
+
+    private HotseatEduController mHotseatEduController;
+
+    public HotseatEduDialog(Context context, AttributeSet attr) {
+        this(context, attr, 0);
+    }
+
+    public HotseatEduDialog(Context context, AttributeSet attrs,
+            int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mContent = this;
+    }
+
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mHotseatWrapper = findViewById(R.id.hotseat_wrapper);
+        mSampleHotseat = findViewById(R.id.sample_prediction);
+
+        DeviceProfile grid = mLauncher.getDeviceProfile();
+        Rect padding = grid.getHotseatLayoutPadding();
+
+        mSampleHotseat.getLayoutParams().height = grid.cellHeightPx;
+        mSampleHotseat.setGridSize(grid.inv.numHotseatIcons, 1);
+        mSampleHotseat.setPadding(padding.left, 0, padding.right, 0);
+
+        Button turnOnBtn = findViewById(R.id.turn_predictions_on);
+        turnOnBtn.setOnClickListener(this::onMigrate);
+
+        Button learnMoreBtn = findViewById(R.id.no_thanks);
+        learnMoreBtn.setOnClickListener(this::onKeepDefault);
+
+    }
+
+    private void onMigrate(View v) {
+        if (mHotseatEduController == null) return;
+        handleClose(true);
+        mHotseatEduController.migrate();
+        mHotseatEduController.finishOnboarding();
+        Toast.makeText(mLauncher, R.string.hotseat_items_migrated, Toast.LENGTH_LONG).show();
+    }
+
+    private void onKeepDefault(View v) {
+        if (mHotseatEduController == null) return;
+        Toast.makeText(getContext(), R.string.hotseat_no_migration, Toast.LENGTH_LONG).show();
+        mHotseatEduController.finishOnboarding();
+        handleClose(true);
+    }
+
+    @Override
+    public void logActionCommand(int command) {
+        // Since this is on-boarding popup, it is not a user controlled action.
+    }
+
+    @Override
+    public int getLogContainerType() {
+        return LauncherLogProto.ContainerType.TIP;
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_ON_BOARD_POPUP) != 0;
+    }
+
+    @Override
+    public void setInsets(Rect insets) {
+        int leftInset = insets.left - mInsets.left;
+        int rightInset = insets.right - mInsets.right;
+        int bottomInset = insets.bottom - mInsets.bottom;
+        mInsets.set(insets);
+        setPadding(leftInset, getPaddingTop(), rightInset, 0);
+        mHotseatWrapper.setPadding(mHotseatWrapper.getPaddingLeft(), getPaddingTop(),
+                mHotseatWrapper.getPaddingRight(), bottomInset);
+        mHotseatWrapper.getLayoutParams().height =
+                mLauncher.getDeviceProfile().hotseatBarSizePx + insets.bottom;
+    }
+
+
+    private void animateOpen() {
+        if (mIsOpen || mOpenCloseAnimator.isRunning()) {
+            return;
+        }
+        mIsOpen = true;
+        mOpenCloseAnimator.setValues(
+                PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
+        mOpenCloseAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mOpenCloseAnimator.start();
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        handleClose(true, DEFAULT_CLOSE_DURATION);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        handleClose(false);
+    }
+
+    /**
+     * Opens User education dialog with a list of suggested apps
+     */
+    public void show(List<WorkspaceItemInfo> predictions) {
+        if (getParent() != null
+                || predictions.size() < mLauncher.getDeviceProfile().inv.numHotseatIcons) {
+            return;
+        }
+        mLauncher.getDragLayer().addView(this);
+        animateOpen();
+        for (int i = 0; i < mLauncher.getDeviceProfile().inv.numHotseatIcons; i++) {
+            WorkspaceItemInfo info = predictions.get(i);
+            PredictedAppIcon icon = PredictedAppIcon.createIcon(mSampleHotseat, info);
+            icon.setEnabled(false);
+            icon.verifyHighRes();
+            CellLayout.LayoutParams lp = new CellLayout.LayoutParams(i, 0, 1, 1);
+            mSampleHotseat.addViewToCellLayout(icon, i, info.getViewId(), lp, true);
+        }
+    }
+
+    /**
+     * Factory method for HotseatPredictionUserEdu dialog
+     */
+    public static HotseatEduDialog getDialog(Launcher launcher) {
+        LayoutInflater layoutInflater = LayoutInflater.from(launcher);
+        return (HotseatEduDialog) layoutInflater.inflate(
+                R.layout.predicted_hotseat_edu, launcher.getDragLayer(),
+                false);
+
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
similarity index 92%
rename from quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
rename to quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index b94142a..8f6081b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.launcher3;
+package com.android.launcher3.hybridhotseat;
 
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 
@@ -35,6 +35,20 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.DragSource;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.ItemInfoWithIcon;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.appprediction.ComponentKeyMapper;
@@ -93,6 +107,7 @@
     private AllAppsStore mAllAppsStore;
     private AnimatorSet mIconRemoveAnimators;
 
+    private HotseatEduController mHotseatEduController;
 
     private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>();
 
@@ -118,6 +133,23 @@
         }
     }
 
+    /**
+     * Returns whether or not the prediction controller is ready to show predictions
+     */
+    public boolean isReady() {
+        return mLauncher.getSharedPrefs().getBoolean(HotseatEduController.KEY_HOTSEAT_EDU_SEEN,
+                false);
+    }
+
+    /**
+     * Transitions to NORMAL workspace mode and shows edu dialog
+     */
+    public void showEduDialog() {
+        if (mHotseatEduController == null) return;
+        mLauncher.getStateManager().goToState(LauncherState.NORMAL, true,
+                () -> mHotseatEduController.showDialog());
+    }
+
     @Override
     public void onViewAttachedToWindow(View view) {
         mLauncher.getDragController().addDragListener(this);
@@ -133,7 +165,7 @@
     }
 
     private void fillGapsWithPrediction(boolean animate, Runnable callback) {
-        if (mDragObject != null) {
+        if (!isReady() || mDragObject != null) {
             return;
         }
         List<WorkspaceItemInfo> predictedApps = mapToWorkspaceItemInfo(mComponentKeyMappers);
@@ -234,6 +266,12 @@
         mAppPredictor.registerPredictionUpdates(mLauncher.getMainExecutor(),
                 this::setPredictedApps);
 
+        if (!isReady()) {
+            if (mHotseatEduController != null) {
+                mHotseatEduController.destroy();
+            }
+            mHotseatEduController = new HotseatEduController(mLauncher);
+        }
         mAppPredictor.requestPredictionUpdate();
     }
 
@@ -244,6 +282,7 @@
         bundle.putParcelableArrayList(BUNDLE_KEY_WORKSPACE, getPinnedAppTargetsInViewGroup(
                 mLauncher.getWorkspace().getScreenWithId(
                         Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets()));
+
         return bundle;
     }
 
@@ -272,7 +311,11 @@
             mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache));
         }
         updateDependencies();
-        fillGapsWithPrediction();
+        if (isReady()) {
+            fillGapsWithPrediction();
+        } else if (mHotseatEduController != null) {
+            mHotseatEduController.setPredictedApps(mapToWorkspaceItemInfo(mComponentKeyMappers));
+        }
     }
 
     private void updateDependencies() {
@@ -466,9 +509,7 @@
     }
 
     @Override
-    public void reapplyItemInfo(ItemInfoWithIcon info) {
-
-    }
+    public void reapplyItemInfo(ItemInfoWithIcon info) {}
 
     @Override
     public void onDropCompleted(View target, DropTarget.DragObject d, boolean success) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 27ac284..bd89626 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
@@ -47,7 +47,7 @@
 
     private static final float RING_EFFECT_RATIO = 0.11f;
 
-    private DeviceProfile mDeviceProfile;
+    private final DeviceProfile mDeviceProfile;
     private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
     private boolean mIsPinned = false;
     private int mNormalizedIconRadius;
@@ -65,7 +65,7 @@
         mDeviceProfile = Launcher.getLauncher(context).getDeviceProfile();
         mNormalizedIconRadius = IconNormalizer.getNormalizedCircleSize(getIconSize()) / 2;
         setOnClickListener(ItemClickHandler.INSTANCE);
-        setOnFocusChangeListener(Launcher.getLauncher(context).mFocusHandler);
+        setOnFocusChangeListener(Launcher.getLauncher(context).getFocusHandler());
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index cae01ae..4b5ba95 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -27,12 +27,12 @@
 
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.HotseatPredictionController;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.RotationMode;
+import com.android.launcher3.hybridhotseat.HotseatPredictionController;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.uioverrides.touchcontrollers.FlingAndHoldTouchController;
 import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
index bd37e56..73c0c97 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -173,7 +173,7 @@
 
     @Override
     public String getDescription(Launcher launcher) {
-        return launcher.getString(R.string.accessibility_desc_recent_apps);
+        return launcher.getString(R.string.accessibility_recent_apps);
     }
 
     public static float getDefaultSwipeHeight(Launcher launcher) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index ad02de1..32855d7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -18,7 +18,7 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE;
@@ -286,7 +286,7 @@
                 }
             });
         }
-        if (QUICKSTEP_SPRINGS.get()) {
+        if (UNSTABLE_SPRINGS.get()) {
             mCurrentAnimation.dispatchOnStartWithVelocity(goingToEnd ? 1f : 0f, velocity);
         }
         anim.start();
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 1b60404..8b5283e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
@@ -21,7 +21,7 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
 import static com.android.quickstep.GestureState.GestureEndTarget.HOME;
@@ -973,7 +973,7 @@
         }
         mLauncherTransitionController.getAnimationPlayer().setDuration(Math.max(0, duration));
 
-        if (QUICKSTEP_SPRINGS.get()) {
+        if (UNSTABLE_SPRINGS.get()) {
             mLauncherTransitionController.dispatchOnStartWithVelocity(end, velocityPxPerMs.y);
         }
         mLauncherTransitionController.getAnimationPlayer().start();
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 29df5cc..71568b3 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -23,6 +23,7 @@
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.config.FeatureFlags.FAKE_LANDSCAPE_UI;
 import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR;
@@ -715,6 +716,7 @@
             pw.println("FeatureFlags:");
             pw.println("  APPLY_CONFIG_AT_RUNTIME=" + APPLY_CONFIG_AT_RUNTIME.get());
             pw.println("  QUICKSTEP_SPRINGS=" + QUICKSTEP_SPRINGS.get());
+            pw.println("  UNSTABLE_SPRINGS=" + UNSTABLE_SPRINGS.get());
             pw.println("  ADAPTIVE_ICON_WINDOW_ANIM=" + ADAPTIVE_ICON_WINDOW_ANIM.get());
             pw.println("  ENABLE_QUICKSTEP_LIVE_TILE=" + ENABLE_QUICKSTEP_LIVE_TILE.get());
             pw.println("  ENABLE_HINTS_IN_OVERVIEW=" + ENABLE_HINTS_IN_OVERVIEW.get());
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index 47bc31a..c836791 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -31,7 +31,7 @@
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
 import static com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
 import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CLEAR_ALL_BUTTON;
@@ -1105,13 +1105,13 @@
 
     private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) {
         addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim);
-        if (QUICKSTEP_SPRINGS.get() && taskView instanceof TaskView)
+        if (UNSTABLE_SPRINGS.get() && taskView instanceof TaskView) {
             addAnim(new SpringObjectAnimator<>(taskView, VIEW_TRANSLATE_Y,
                             MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
                             SpringForce.STIFFNESS_MEDIUM,
                             0, -taskView.getHeight()),
                     duration, LINEAR, anim);
-        else {
+        } else {
             addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()),
                     duration, LINEAR, anim);
         }
@@ -1185,7 +1185,7 @@
                 }
                 int scrollDiff = newScroll[i] - oldScroll[i] + offset;
                 if (scrollDiff != 0) {
-                    if (QUICKSTEP_SPRINGS.get() && child instanceof TaskView) {
+                    if (UNSTABLE_SPRINGS.get() && child instanceof TaskView) {
                         addAnim(new SpringObjectAnimator<>(child, VIEW_TRANSLATE_X,
                                 MIN_VISIBLE_CHANGE_PIXELS, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY,
                                 SpringForce.STIFFNESS_MEDIUM,
diff --git a/quickstep/res/drawable/ic_bulb_outline.xml b/quickstep/res/drawable/ic_bulb_outline.xml
deleted file mode 100644
index ef7bf9a..0000000
--- a/quickstep/res/drawable/ic_bulb_outline.xml
+++ /dev/null
@@ -1,29 +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.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="192dp"
-    android:height="192dp"
-    android:viewportWidth="192"
-    android:viewportHeight="192">
-    <path
-        android:pathData="M96,90.01"
-        android:strokeWidth="2.4297"
-        android:fillColor="#00000000"
-        android:strokeColor="#FFC800"/>
-    <path
-        android:pathData="M153.24,48.09c-0.5,-1.51 -0.99,-2.86 -1.51,-4.12c-3.03,-7.1 -7.26,-13.45 -12.59,-18.88C127.68,13.42 112.4,7 96.1,7c-14,0 -27.66,4.93 -38.45,13.87C47,29.69 39.61,41.99 36.82,55.51c-0.76,3.9 -1.14,7.86 -1.14,11.78c0,16.39 6.36,31.77 17.9,43.3c2.65,2.65 5.59,5.08 8.74,7.23l0.09,24.14v22.03c0,5.33 4.82,10.01 10.32,10.01h0.41h3.85v0c0,6.23 4.53,10.93 10.53,10.93h16.94c6.01,0 10.54,-4.7 10.54,-10.93v0h4.31c5.56,0 10.26,-4.5 10.26,-9.83v-22.01c0.01,-0.06 0.01,-0.13 0.01,-0.2v-23.74c16.75,-11.15 26.73,-30.15 26.73,-50.94C156.31,60.76 155.28,54.3 153.24,48.09zM118.51,111.08l-0.46,0.29l-0.46,0.29v0.55v0.55v4.38v22.77l-14.12,0V95.9h14h2v-2v-8.5v-2h-2H74.53h-2v2v8.5v2h2h14v44.02l-14.13,0l-0.09,-23.21l-0.02,-4.3l0,-0.54l0,-0.54l-0.45,-0.29l-0.45,-0.29l-3.6,-2.36c-2.81,-1.84 -5.41,-3.95 -7.73,-6.28c-9.27,-9.26 -14.38,-21.63 -14.38,-34.82c0,-3.14 0.3,-6.31 0.9,-9.43C53.25,35.35 73.23,19 96.1,19c13.05,0 25.3,5.15 34.48,14.5c4.27,4.34 7.66,9.43 10.08,15.1c0.39,0.96 0.78,2.03 1.18,3.24c1.64,5 2.47,10.2 2.47,15.45c0,17.1 -8.27,32.58 -22.11,41.43L118.51,111.08z"
-        android:fillColor="#4285F4"/>
-</vector>
\ No newline at end of file
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 378858f..2b21df8 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -29,9 +29,6 @@
     <!-- Title for an option to enter freeform mode for a given app -->
     <string name="recent_task_option_freeform">Freeform</string>
 
-    <!-- Content description for the recent apps panel (not shown on the screen). [CHAR LIMIT=NONE] -->
-    <string name="accessibility_desc_recent_apps">Overview</string>
-
     <!-- Recents: The empty recents string. [CHAR LIMIT=NONE] -->
     <string name="recents_empty_message">No recent items</string>
 
@@ -67,14 +64,30 @@
     <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="back_gesture_tutorial_close_button_content_description" translatable="false">Close</string>
 
-    <!-- Title shown on the notification of Back gesture tutorial. [CHAR LIMIT=30] -->
-    <string name="back_gesture_tutorial_notification_title" translatable="false">Try the new back gesture</string>
-    <!-- Subtitle shown on the notification of Back gesture tutorial. [CHAR LIMIT=60] -->
-    <string name="back_gesture_tutorial_notification_subtitle" translatable="false">Learn how to go back while using your apps</string>
-    <!-- Action text shown on the notification of Back gesture tutorial. [CHAR LIMIT=14] -->
-    <string name="back_gesture_tutorial_notification_action_label" translatable="false">Try it</string>
+    <!-- Hotseat migration notification title -->
+    <string translatable="false" name="hotseat_migrate_prompt_title">Get suggested apps on the home screen</string>
+    <!-- Hotseat migration notification content -->
+    <string translatable="false" name="hotseat_migrate_prompt_content">Tap to set up</string>
+    <!-- Hotseat migration wizard title -->
+    <string translatable="false" name="hotseat_migrate_title">Suggested apps replace the bottom row of apps</string>
+    <!-- Hotseat migration wizard message -->
+    <string translatable="false" name="hotseat_migrate_message">To pin a favorite app, drag it over a suggested app. To hide a suggested app, touch &amp; hold it.</string>
+    <!-- Toast message user sees after opting into fully predicted hybrid hotseat -->
+    <string translatable="false" name="hotseat_items_migrated">Your hotseat items have been moved to the last page.</string>
+    <!-- Toast message user sees after opting into fully predicted hybrid hotseat -->
+    <string translatable="false" name="hotseat_no_migration">You can remove items from the hotseat manually to see suggested apps in their spot.</string>
+    <!-- Button text to opt in for fully predicted hotseat -->
+    <string translatable="false" name="hotseat_migrate_accept">I\'m in</string>
+    <!-- Button text to dismiss opt in for fully predicted hotseat -->
+    <string translatable="false" name="hotseat_migrate_dismiss">No thanks</string>
+    <!-- Hotseat onboard notification title -->
+    <string translatable="false" name="hotseat_onboard_notification_title">Your hotseat just got smarter</string>
+    <!-- Hotseat onboard notification detail -->
+    <string translatable="false" name="hotseat_onboard_notification_detail">Tap here to set it up</string>
+
+
 
     <!-- Title shown during interactive part of Back gesture tutorial for right edge. [CHAR LIMIT=30] -->
     <string name="back_gesture_tutorial_playground_title_swipe_inward_right_edge" translatable="false">Try the back gesture</string>
diff --git a/quickstep/res/xml/notification_action.xml b/quickstep/res/xml/notification_action.xml
deleted file mode 100644
index cc3612e..0000000
--- a/quickstep/res/xml/notification_action.xml
+++ /dev/null
@@ -1,19 +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.
--->
-<alias xmlns:android="http://schemas.android.com/apk/res/android">
-    <intent
-        android:action="com.android.quickstep.action.BACK_GESTURE_TUTORIAL" />
-</alias>
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 99b2a81..d5ce734 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -25,7 +25,7 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.TimeInterpolator;
@@ -277,7 +277,7 @@
     private void handleFirstSwipeToOverview(final ValueAnimator animator,
             final long expectedDuration, final LauncherState targetState, final float velocity,
             final boolean isFling) {
-        if (QUICKSTEP_SPRINGS.get() && mFromState == OVERVIEW && mToState == ALL_APPS
+        if (UNSTABLE_SPRINGS.get() && mFromState == OVERVIEW && mToState == ALL_APPS
                 && targetState == OVERVIEW) {
             mFinishFastOnSecondTouch = true;
         } else  if (mFromState == NORMAL && mToState == OVERVIEW && targetState == OVERVIEW) {
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialActivity.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialActivity.java
new file mode 100644
index 0000000..295ab48
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialActivity.java
@@ -0,0 +1,73 @@
+/*
+ * 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.graphics.Color;
+import android.os.Bundle;
+import android.view.View;
+import android.view.Window;
+
+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 java.util.Optional;
+
+/** Shows the Back gesture interactive tutorial in full screen mode. */
+public class BackGestureTutorialActivity extends FragmentActivity {
+
+    Optional<BackGestureTutorialFragment> mFragment = Optional.empty();
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.back_gesture_tutorial_activity);
+
+        mFragment = Optional.of(BackGestureTutorialFragment.newInstance(TutorialStep.ENGAGED,
+                TutorialType.RIGHT_EDGE_BACK_NAVIGATION));
+        getSupportFragmentManager().beginTransaction()
+                .add(R.id.back_gesture_tutorial_fragment_container, mFragment.get())
+                .commit();
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus) {
+        super.onWindowFocusChanged(hasFocus);
+        if (hasFocus) {
+            hideSystemUI();
+        }
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (mFragment.isPresent()) {
+            mFragment.get().onBackPressed();
+        }
+    }
+
+    private void hideSystemUI() {
+        getWindow().getDecorView().setSystemUiVisibility(
+                View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+                        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                        | View.SYSTEM_UI_FLAG_FULLSCREEN);
+        getWindow().setNavigationBarColor(Color.TRANSPARENT);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialConfirmController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialConfirmController.java
new file mode 100644
index 0000000..486d676
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialConfirmController.java
@@ -0,0 +1,64 @@
+/*
+ * 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
new file mode 100644
index 0000000..3fe91a3
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -0,0 +1,165 @@
+/*
+ * 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.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 java.util.Optional;
+
+/**
+ * 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);
+    }
+
+    void transitToController() {
+        updateTitles();
+        updateActionButtons();
+    }
+
+    void hideHandCoachingAnimation() {
+        mHandCoachingAnimation.stop();
+    }
+
+    void onGestureDetected() {
+        hideHandCoachingAnimation();
+
+        if (mTutorialStep == TutorialStep.CONFIRM) {
+            mFragment.closeTutorial();
+            return;
+        }
+
+        if (mTutorialTypeInfo.get().getTutorialType() == TutorialType.RIGHT_EDGE_BACK_NAVIGATION) {
+            mFragment.changeController(TutorialStep.ENGAGED,
+                    TutorialType.LEFT_EDGE_BACK_NAVIGATION);
+            return;
+        }
+
+        mFragment.changeController(TutorialStep.CONFIRM);
+    }
+
+    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);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialEngagedController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialEngagedController.java
new file mode 100644
index 0000000..c9ee1e2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialEngagedController.java
@@ -0,0 +1,64 @@
+/*
+ * 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
new file mode 100644
index 0000000..54408ce
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
@@ -0,0 +1,165 @@
+/*
+ * 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.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.fragment.app.Fragment;
+
+import com.android.launcher3.R;
+
+import java.net.URISyntaxException;
+import java.util.Optional;
+
+/** Shows the Back gesture interactive tutorial. */
+public class BackGestureTutorialFragment extends Fragment {
+
+    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;
+
+    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;
+    }
+
+    @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);
+    }
+
+    @Override
+    public View onCreateView(
+            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);
+        mHandCoachingAnimation = new BackGestureTutorialHandAnimation(getContext(), mRootView);
+
+        return mRootView;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        changeController(mTutorialStep, mTutorialType);
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mHandCoachingAnimation.stop();
+    }
+
+    @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;
+    }
+
+    void onBackPressed() {
+        if (mTutorialController.isPresent()) {
+            mTutorialController.get().onGestureDetected();
+        }
+    }
+
+    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);
+        }
+    }
+
+    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/BackGestureTutorialHandAnimation.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialHandAnimation.java
new file mode 100644
index 0000000..d03811d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialHandAnimation.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.interaction;
+
+import android.content.Context;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.widget.ImageView;
+
+import androidx.core.content.ContextCompat;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+
+import java.time.Duration;
+
+/** Hand coaching animation. */
+final class BackGestureTutorialHandAnimation {
+
+    // A delay for waiting the Activity fully launches.
+    private static final Duration ANIMATION_START_DELAY = Duration.ofMillis(300L);
+
+    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);
+    }
+
+    boolean isRunning() {
+        return mGestureAnimation.isRunning();
+    }
+
+    /**
+     * Starts animation if the playground is launched for the first time.
+     */
+    void maybeStartLoopedAnimation(TutorialType tutorialType) {
+        if (isRunning() || mIsAnimationPlayed) {
+            return;
+        }
+
+        mIsAnimationPlayed = true;
+        clearAnimationCallbacks();
+        mGestureAnimation.registerAnimationCallback(
+                new Animatable2.AnimationCallback() {
+                    @Override
+                    public void onAnimationEnd(Drawable drawable) {
+                        super.onAnimationEnd(drawable);
+                        mGestureAnimation.start();
+                    }
+                });
+        start(tutorialType);
+    }
+
+    private void start(TutorialType tutorialType) {
+        // Because the gesture animation has only the right side form.
+        // The left side form of the gesture animation is made from flipping the View.
+        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();
+    }
+
+    void stop() {
+        mIsAnimationPlayed = false;
+        clearAnimationCallbacks();
+        mGestureAnimation.stop();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfo.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfo.java
new file mode 100644
index 0000000..ac8443d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfo.java
@@ -0,0 +1,109 @@
+/*
+ * 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
new file mode 100644
index 0000000..9575d83
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfoProvider.java
@@ -0,0 +1,59 @@
+/*
+ * 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/res/values/strings.xml b/res/values/strings.xml
index dec8939..218f6db 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -103,7 +103,7 @@
     <!-- Label for install drop target. [CHAR_LIMIT=20] -->
     <string name="install_drop_target_label">Install</string>
     <!-- Label for install dismiss prediction. -->
-    <string translatable="false" name="dismiss_prediction_label">Dismiss prediction</string>
+    <string translatable="false" name="dismiss_prediction_label">Don\'t suggest app</string>
     <!-- Label for pinning predicted app. -->
     <string name="pin_prediction" translatable="false">Pin Prediction</string>
 
diff --git a/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
new file mode 100644
index 0000000..f769055
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.folder;
+
+import static org.junit.Assert.assertTrue;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.shadows.LShadowUserManager;
+import com.android.launcher3.util.LauncherRoboTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+
+@RunWith(LauncherRoboTestRunner.class)
+public final class FolderNameProviderTest {
+    private Context mContext;
+    private WorkspaceItemInfo mItem1;
+    private WorkspaceItemInfo mItem2;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mItem1 = new WorkspaceItemInfo(new AppInfo(
+                new ComponentName("a.b.c", "a.b.c/a.b.c.d"),
+                "title1",
+                LShadowUserManager.newUserHandle(10),
+                new Intent().setComponent(new ComponentName("a.b.c", "a.b.c/a.b.c.d"))
+        ));
+        mItem2 = new WorkspaceItemInfo(new AppInfo(
+                new ComponentName("a.b.c", "a.b.c/a.b.c.d"),
+                "title2",
+                LShadowUserManager.newUserHandle(10),
+                new Intent().setComponent(new ComponentName("a.b.c", "a.b.c/a.b.c.d"))
+        ));
+    }
+
+    @Test
+    public void getSuggestedFolderName_workAssignedToEnd() {
+        ArrayList<WorkspaceItemInfo> list = new ArrayList<>();
+        list.add(mItem1);
+        list.add(mItem2);
+        String[] suggestedNameOut = new String[FolderNameProvider.SUGGEST_MAX];
+        new FolderNameProvider().getSuggestedFolderName(mContext, list, suggestedNameOut);
+        assertTrue(suggestedNameOut[0].equals("Work"));
+
+        suggestedNameOut[0] = "candidate1";
+        suggestedNameOut[1] = "candidate2";
+        suggestedNameOut[2] = "candidate3";
+        new FolderNameProvider().getSuggestedFolderName(mContext, list, suggestedNameOut);
+        assertTrue(suggestedNameOut[3].equals("Work"));
+
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
index ea7c137..b7f2243 100644
--- a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
@@ -127,7 +127,7 @@
     @Test
     public void testAddItem_some_items_added() throws Exception {
         Callbacks callbacks = mock(Callbacks.class);
-        mModelHelper.getModel().initialize(callbacks);
+        mModelHelper.getModel().addCallbacks(callbacks);
 
         WorkspaceItemInfo info = new WorkspaceItemInfo();
         info.intent = new Intent().setComponent(mComponent1);
diff --git a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
index e0ddcb1..f8ac010 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
@@ -16,8 +16,9 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
 import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.mock;
 import static org.robolectric.Shadows.shadowOf;
 import static org.robolectric.util.ReflectionHelpers.setField;
 
@@ -26,14 +27,10 @@
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageInstaller.SessionInfo;
 import android.content.pm.PackageInstaller.SessionParams;
-import android.net.Uri;
-import android.provider.Settings;
 
 import com.android.launcher3.FolderInfo;
-import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherProvider;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.model.BgDataModel.Callbacks;
@@ -48,12 +45,7 @@
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.LooperMode;
 import org.robolectric.annotation.LooperMode.Mode;
-import org.robolectric.shadows.ShadowPackageManager;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.OutputStreamWriter;
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 
 /**
@@ -63,40 +55,22 @@
 @LooperMode(Mode.PAUSED)
 public class DefaultLayoutProviderTest {
 
-    private static final String SETTINGS_APP = "com.android.settings";
-    private static final String TEST_PROVIDER_AUTHORITY =
-            DefaultLayoutProviderTest.class.getName().toLowerCase();
-
-    private static final int BITMAP_SIZE = 10;
-    private static final int GRID_SIZE = 4;
-
     private LauncherModelHelper mModelHelper;
     private Context mTargetContext;
-    private InvariantDeviceProfile mIdp;
 
     @Before
     public void setUp() {
         mModelHelper = new LauncherModelHelper();
         mTargetContext = RuntimeEnvironment.application;
 
-        mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
-        mIdp.numRows = mIdp.numColumns = mIdp.numHotseatIcons = GRID_SIZE;
-        mIdp.iconBitmapSize = BITMAP_SIZE;
-
-        mModelHelper.provider.setAllowLoadDefaultFavorites(true);
-        Settings.Secure.putString(mTargetContext.getContentResolver(),
-                "launcher3.layout.provider", TEST_PROVIDER_AUTHORITY);
-
-        ShadowPackageManager spm = shadowOf(mTargetContext.getPackageManager());
-        spm.addProviderIfNotPresent(new ComponentName("com.test", "Dummy")).authority =
-                TEST_PROVIDER_AUTHORITY;
-        spm.addActivityIfNotPresent(new ComponentName(SETTINGS_APP, SETTINGS_APP));
+        shadowOf(mTargetContext.getPackageManager())
+                .addActivityIfNotPresent(new ComponentName(TEST_PACKAGE, TEST_PACKAGE));
     }
 
     @Test
     public void testCustomProfileLoaded_with_icon_on_hotseat() throws Exception {
         writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0)
-                .putApp(SETTINGS_APP, SETTINGS_APP));
+                .putApp(TEST_PACKAGE, TEST_PACKAGE));
 
         // Verify one item in hotseat
         assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
@@ -108,9 +82,9 @@
     @Test
     public void testCustomProfileLoaded_with_folder() throws Exception {
         writeLayoutAndLoad(new LauncherLayoutBuilder().atHotseat(0).putFolder(android.R.string.copy)
-                .addApp(SETTINGS_APP, SETTINGS_APP)
-                .addApp(SETTINGS_APP, SETTINGS_APP)
-                .addApp(SETTINGS_APP, SETTINGS_APP)
+                .addApp(TEST_PACKAGE, TEST_PACKAGE)
+                .addApp(TEST_PACKAGE, TEST_PACKAGE)
+                .addApp(TEST_PACKAGE, TEST_PACKAGE)
                 .build());
 
         // Verify folder
@@ -146,19 +120,13 @@
     }
 
     private void writeLayoutAndLoad(LauncherLayoutBuilder builder) throws Exception {
-        ByteArrayOutputStream bos = new ByteArrayOutputStream();
-        builder.build(new OutputStreamWriter(bos));
-
-        Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, mTargetContext);
-        shadowOf(mTargetContext.getContentResolver()).registerInputStream(layoutUri,
-                new ByteArrayInputStream(bos.toByteArray()));
+        mModelHelper.setupDefaultLayoutProvider(builder);
 
         LoaderResults results = new LoaderResults(
                 LauncherAppState.getInstance(mTargetContext),
                 mModelHelper.getBgDataModel(),
                 mModelHelper.getAllAppsList(),
-                0,
-                new WeakReference<>(mock(Callbacks.class)));
+                new Callbacks[0]);
         LoaderTask task = new LoaderTask(
                 LauncherAppState.getInstance(mTargetContext),
                 mModelHelper.getAllAppsList(),
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
index 8dd7588..1ed4bca 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
@@ -190,7 +190,7 @@
 
     @Test
     public void testWorkspace_items_not_merged_in_next_screen() throws Exception {
-        // First screen has 2 items that need to be moved, but second screen has only one
+        // First screen has 2 mItems that need to be moved, but second screen has only one
         // empty space after migration (top-left corner)
         int[][][] ids = mModelHelper.createGrid(new int[][][]{{
                 {  0,  0,  0,  1},
@@ -277,7 +277,7 @@
     }
 
     /**
-     * Verifies that the workspace items are arranged in the provided order.
+     * Verifies that the workspace mItems are arranged in the provided order.
      * @param ids A 3d array where the first dimension represents the screen, and the rest two
      *            represent the workspace grid.
      */
diff --git a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 4854314..7fa3ee9 100644
--- a/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -46,7 +46,6 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.LauncherApps;
 import android.database.MatrixCursor;
 import android.os.Process;
 
@@ -77,7 +76,6 @@
     private MatrixCursor mCursor;
     private InvariantDeviceProfile mIDP;
     private Context mContext;
-    private LauncherApps mLauncherApps;
 
     private LoaderCursor mLoaderCursor;
 
@@ -86,7 +84,6 @@
         mContext = RuntimeEnvironment.application;
         mIDP = InvariantDeviceProfile.INSTANCE.get(mContext);
         mApp = LauncherAppState.getInstance(mContext);
-        mLauncherApps = mContext.getSystemService(LauncherApps.class);
 
         mCursor = new MatrixCursor(new String[] {
                 ICON, ICON_PACKAGE, ICON_RESOURCE, TITLE,
@@ -174,7 +171,7 @@
         mIDP.numColumns = 4;
         mIDP.numHotseatIcons = 3;
 
-        // Overlapping items are not placed
+        // Overlapping mItems are not placed
         assertTrue(mLoaderCursor.checkItemPlacement(
                 newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1)));
         assertFalse(mLoaderCursor.checkItemPlacement(
@@ -200,7 +197,7 @@
         mIDP.numColumns = 4;
         mIDP.numHotseatIcons = 3;
 
-        // Hotseat items are only placed based on screenId
+        // Hotseat mItems are only placed based on screenId
         assertTrue(mLoaderCursor.checkItemPlacement(
                 newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 1)));
         assertTrue(mLoaderCursor.checkItemPlacement(
diff --git a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
new file mode 100644
index 0000000..c7979b2
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import static com.android.launcher3.util.Executors.createAndStartNewForegroundLooper;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.spy;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.os.Process;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.PagedView;
+import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.LauncherLayoutBuilder;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
+import com.android.launcher3.util.LooperExecutor;
+import com.android.launcher3.util.ViewOnDrawExecutor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Tests to verify multiple callbacks in Loader
+ */
+@RunWith(LauncherRoboTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class ModelMultiCallbacksTest {
+
+    private LauncherModelHelper mModelHelper;
+
+    private ShadowPackageManager mSpm;
+    private LooperExecutor mTempMainExecutor;
+
+    @Before
+    public void setUp() throws Exception {
+        mModelHelper = new LauncherModelHelper();
+        mModelHelper.installApp(TEST_PACKAGE);
+
+        mSpm = shadowOf(RuntimeEnvironment.application.getPackageManager());
+
+        // Since robolectric tests run on main thread, we run the loader-UI calls on a temp thread,
+        // so that we can wait appropriately for the loader to complete.
+        mTempMainExecutor = new LooperExecutor(createAndStartNewForegroundLooper("tempMain"));
+        ReflectionHelpers.setField(mModelHelper.getModel(), "mMainExecutor", mTempMainExecutor);
+    }
+
+    @Test
+    public void testTwoCallbacks_loadedTogether() throws Exception {
+        setupWorkspacePages(3);
+
+        MyCallbacks cb1 = spy(MyCallbacks.class);
+        mModelHelper.getModel().addCallbacksAndLoad(cb1);
+
+        waitForLoaderAndTempMainThread();
+        cb1.verifySynchronouslyBound(3);
+
+        // Add a new callback
+        cb1.reset();
+        MyCallbacks cb2 = spy(MyCallbacks.class);
+        cb2.mPageToBindSync = 2;
+        mModelHelper.getModel().addCallbacksAndLoad(cb2);
+
+        waitForLoaderAndTempMainThread();
+        cb1.verifySynchronouslyBound(3);
+        cb2.verifySynchronouslyBound(3);
+
+        // Remove callbacks
+        cb1.reset();
+        cb2.reset();
+
+        // No effect on callbacks when removing an callback
+        mModelHelper.getModel().removeCallbacks(cb2);
+        waitForLoaderAndTempMainThread();
+        assertNull(cb1.mDeferredExecutor);
+        assertNull(cb2.mDeferredExecutor);
+
+        // Reloading only loads registered callbacks
+        mModelHelper.getModel().startLoader();
+        waitForLoaderAndTempMainThread();
+        cb1.verifySynchronouslyBound(3);
+        assertNull(cb2.mDeferredExecutor);
+    }
+
+    @Test
+    public void testTwoCallbacks_receiveUpdates() throws Exception {
+        setupWorkspacePages(1);
+
+        MyCallbacks cb1 = spy(MyCallbacks.class);
+        MyCallbacks cb2 = spy(MyCallbacks.class);
+        mModelHelper.getModel().addCallbacksAndLoad(cb1);
+        mModelHelper.getModel().addCallbacksAndLoad(cb2);
+        waitForLoaderAndTempMainThread();
+
+        cb1.verifyApps(TEST_PACKAGE);
+        cb2.verifyApps(TEST_PACKAGE);
+
+        // Install package 1
+        String pkg1 = "com.test.pkg1";
+        mModelHelper.installApp(pkg1);
+        mModelHelper.getModel().onPackageAdded(pkg1, Process.myUserHandle());
+        waitForLoaderAndTempMainThread();
+        cb1.verifyApps(TEST_PACKAGE, pkg1);
+        cb2.verifyApps(TEST_PACKAGE, pkg1);
+
+        // Install package 2
+        String pkg2 = "com.test.pkg2";
+        mModelHelper.installApp(pkg2);
+        mModelHelper.getModel().onPackageAdded(pkg2, Process.myUserHandle());
+        waitForLoaderAndTempMainThread();
+        cb1.verifyApps(TEST_PACKAGE, pkg1, pkg2);
+        cb2.verifyApps(TEST_PACKAGE, pkg1, pkg2);
+
+        // Uninstall package 2
+        mSpm.removePackage(pkg1);
+        mModelHelper.getModel().onPackageRemoved(pkg1, Process.myUserHandle());
+        waitForLoaderAndTempMainThread();
+        cb1.verifyApps(TEST_PACKAGE, pkg2);
+        cb2.verifyApps(TEST_PACKAGE, pkg2);
+
+        // Unregister a callback and verify updates no longer received
+        mModelHelper.getModel().removeCallbacks(cb2);
+        mSpm.removePackage(pkg2);
+        mModelHelper.getModel().onPackageRemoved(pkg2, Process.myUserHandle());
+        waitForLoaderAndTempMainThread();
+        cb1.verifyApps(TEST_PACKAGE);
+        cb2.verifyApps(TEST_PACKAGE, pkg2);
+    }
+
+    private void waitForLoaderAndTempMainThread() throws Exception {
+        Executors.MODEL_EXECUTOR.submit(() -> { }).get();
+        mTempMainExecutor.submit(() -> { }).get();
+    }
+
+    private void setupWorkspacePages(int pageCount) throws Exception {
+        // Create a layout with 3 pages
+        LauncherLayoutBuilder builder = new LauncherLayoutBuilder();
+        for (int i = 0; i < pageCount; i++) {
+            builder.atWorkspace(1, 1, i).putApp(TEST_PACKAGE, TEST_PACKAGE);
+        }
+        mModelHelper.setupDefaultLayoutProvider(builder);
+    }
+
+    private abstract static class MyCallbacks implements Callbacks {
+
+        final List<ItemInfo> mItems = new ArrayList<>();
+        int mPageToBindSync = 0;
+        int mPageBoundSync = PagedView.INVALID_PAGE;
+        ViewOnDrawExecutor mDeferredExecutor;
+        AppInfo[] mAppInfos;
+
+        MyCallbacks() { }
+
+        @Override
+        public void onPageBoundSynchronously(int page) {
+            mPageBoundSync = page;
+        }
+
+        @Override
+        public void executeOnNextDraw(ViewOnDrawExecutor executor) {
+            mDeferredExecutor = executor;
+        }
+
+        @Override
+        public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) {
+            mItems.addAll(shortcuts);
+        }
+
+        @Override
+        public void bindAllApplications(AppInfo[] apps) {
+            mAppInfos = apps;
+        }
+
+        @Override
+        public int getPageToBindSynchronously() {
+            return mPageToBindSync;
+        }
+
+        public void reset() {
+            mItems.clear();
+            mPageBoundSync = PagedView.INVALID_PAGE;
+            mDeferredExecutor = null;
+            mAppInfos = null;
+        }
+
+        public void verifySynchronouslyBound(int totalItems) {
+            // Verify that the requested page is bound synchronously
+            assertEquals(mPageBoundSync, mPageToBindSync);
+            assertEquals(mItems.size(), 1);
+            assertEquals(mItems.get(0).screenId, mPageBoundSync);
+            assertNotNull(mDeferredExecutor);
+
+            // Verify that all other pages are bound properly
+            mDeferredExecutor.runAllTasks();
+            assertEquals(mItems.size(), totalItems);
+        }
+
+        public void verifyApps(String... apps) {
+            assertEquals(apps.length, mAppInfos.length);
+            assertEquals(Arrays.stream(mAppInfos)
+                    .map(ai -> ai.getTargetComponent().getPackageName())
+                    .collect(Collectors.toSet()),
+                    new HashSet<>(Arrays.asList(apps)));
+        }
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
index ccbc18a..166e28b 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
@@ -43,6 +43,7 @@
 
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * Extension of {@link ShadowLauncherApps} with missing shadow methods
@@ -93,4 +94,26 @@
         return RuntimeEnvironment.application.getPackageManager()
                 .getApplicationInfo(packageName, flags);
     }
+
+    @Implementation
+    public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) {
+        Intent intent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_LAUNCHER)
+                .setPackage(packageName);
+        return RuntimeEnvironment.application.getPackageManager().queryIntentActivities(intent, 0)
+                .stream()
+                .map(ri -> getLauncherActivityInfo(ri.activityInfo))
+                .collect(Collectors.toList());
+    }
+
+    @Implementation
+    public boolean hasShortcutHostPermission() {
+        return true;
+    }
+
+    @Override
+    protected List<LauncherActivityInfo> getShortcutConfigActivityList(String packageName,
+            UserHandle user) {
+        return Collections.emptyList();
+    }
 }
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
index edf8edb..576ddbd 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowUserManager.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.shadows;
 
+import android.os.Parcel;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.SparseBooleanArray;
@@ -50,4 +51,12 @@
     public void setUserLocked(UserHandle userHandle, boolean enabled) {
         mLockedUsers.put(userHandle.hashCode(), enabled);
     }
+
+    // Create user handle from parcel since UserHandle.of() was only added in later APIs.
+    public static UserHandle newUserHandle(int uid) {
+        Parcel userParcel = Parcel.obtain();
+        userParcel.writeInt(uid);
+        userParcel.setDataPosition(0);
+        return new UserHandle(userParcel);
+    }
 }
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
index 1a03f9f..655055c 100644
--- a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -20,13 +20,19 @@
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
 
 import android.content.ComponentName;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.provider.Settings;
 
 import com.android.launcher3.AppInfo;
+import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
@@ -40,10 +46,14 @@
 import org.robolectric.Robolectric;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.shadows.ShadowContentResolver;
+import org.robolectric.shadows.ShadowPackageManager;
 import org.robolectric.util.ReflectionHelpers;
 
 import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
 import java.lang.reflect.Field;
 import java.util.HashMap;
 import java.util.List;
@@ -63,6 +73,13 @@
     public static final int NO__ICON = -1;
     public static final String TEST_PACKAGE = "com.android.launcher3.validpackage";
 
+    // Authority for providing a dummy default-workspace-layout data.
+    private static final String TEST_PROVIDER_AUTHORITY =
+            LauncherModelHelper.class.getName().toLowerCase();
+    private static final int DEFAULT_BITMAP_SIZE = 10;
+    private static final int DEFAULT_GRID_SIZE = 4;
+
+
     private final HashMap<Class, HashMap<String, Field>> mFieldCache = new HashMap<>();
     public final TestLauncherProvider provider;
 
@@ -285,4 +302,42 @@
 
         return ids;
     }
+
+    /**
+     * Sets up a dummy provider to load the provided layout by default, next time the layout loads
+     */
+    public void setupDefaultLayoutProvider(LauncherLayoutBuilder builder) throws Exception {
+        Context context = RuntimeEnvironment.application;
+        InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context);
+        idp.numRows = idp.numColumns = idp.numHotseatIcons = DEFAULT_GRID_SIZE;
+        idp.iconBitmapSize = DEFAULT_BITMAP_SIZE;
+
+        provider.setAllowLoadDefaultFavorites(true);
+        Settings.Secure.putString(context.getContentResolver(),
+                "launcher3.layout.provider", TEST_PROVIDER_AUTHORITY);
+
+        shadowOf(context.getPackageManager())
+                .addProviderIfNotPresent(new ComponentName("com.test", "Dummy")).authority =
+                TEST_PROVIDER_AUTHORITY;
+
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        builder.build(new OutputStreamWriter(bos));
+        Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, context);
+        shadowOf(context.getContentResolver()).registerInputStream(layoutUri,
+                new ByteArrayInputStream(bos.toByteArray()));
+    }
+
+    /**
+     * Simulates an apk install with a default main activity with same class and package name
+     */
+    public void installApp(String component) throws NameNotFoundException {
+        ShadowPackageManager spm = shadowOf(RuntimeEnvironment.application.getPackageManager());
+        ComponentName cn = new ComponentName(component, component);
+        spm.addActivityIfNotPresent(cn);
+
+        IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN);
+        filter.addCategory(Intent.CATEGORY_LAUNCHER);
+        filter.addCategory(Intent.CATEGORY_DEFAULT);
+        spm.addIntentFilterForActivity(cn, filter);
+    }
 }
diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java
index af219ba..f76ca50 100644
--- a/src/com/android/launcher3/AppInfo.java
+++ b/src/com/android/launcher3/AppInfo.java
@@ -26,6 +26,8 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageManagerHelper;
 
@@ -89,6 +91,15 @@
         runtimeStatusFlags = info.runtimeStatusFlags;
     }
 
+    @VisibleForTesting
+    public AppInfo(ComponentName componentName, CharSequence title,
+            UserHandle user, Intent intent) {
+        this.componentName = componentName;
+        this.title = title;
+        this.user = user;
+        this.intent = intent;
+    }
+
     @Override
     protected String dumpProperties() {
         return super.dumpProperties() + " componentName=" + componentName;
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
index bd48aec..423f2bb 100644
--- a/src/com/android/launcher3/DeleteDropTarget.java
+++ b/src/com/android/launcher3/DeleteDropTarget.java
@@ -126,7 +126,8 @@
             onAccessibilityDrop(null, item);
             ModelWriter modelWriter = mLauncher.getModelWriter();
             Runnable onUndoClicked = () -> {
-                modelWriter.abortDelete(itemPage);
+                mLauncher.setPageToBindSynchronously(itemPage);
+                modelWriter.abortDelete();
                 mLauncher.getUserEventDispatcher().logActionOnControl(TAP, UNDO);
             };
             Snackbar.show(mLauncher, R.string.item_removed, R.string.undo,
diff --git a/src/com/android/launcher3/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java
index 8b6d209..5b453c3 100644
--- a/src/com/android/launcher3/ExtendedEditText.java
+++ b/src/com/android/launcher3/ExtendedEditText.java
@@ -44,7 +44,7 @@
      * Implemented by listeners of the back key.
      */
     public interface OnBackKeyListener {
-        public boolean onBackKey();
+        boolean onBackKey();
     }
 
     private OnBackKeyListener mBackKeyListener;
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 03ee707..b89e727 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -49,12 +49,17 @@
         super(context, attrs, defStyle);
     }
 
-    /* Get the orientation specific coordinates given an invariant order in the hotseat. */
-    int getCellXFromOrder(int rank) {
+    /**
+     * Returns orientation specific cell X given invariant order in the hotseat
+     */
+    public int getCellXFromOrder(int rank) {
         return mHasVerticalHotseat ? 0 : rank;
     }
 
-    int getCellYFromOrder(int rank) {
+    /**
+     * Returns orientation specific cell Y given invariant order in the hotseat
+     */
+    public int getCellYFromOrder(int rank) {
         return mHasVerticalHotseat ? (getCountY() - (rank + 1)) : 0;
     }
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index d8c4c5c..f5fafbf 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -292,6 +292,7 @@
     private PopupDataProvider mPopupDataProvider;
 
     private int mSynchronouslyBoundPage = PagedView.INVALID_PAGE;
+    private int mPageToBindSynchronously = PagedView.INVALID_PAGE;
 
     // We only want to get the SharedPreferences once since it does an FS stat each time we get
     // it from the context.
@@ -307,7 +308,7 @@
     // Request id for any pending activity result
     protected int mPendingActivityRequestCode = -1;
 
-    public ViewGroupFocusHelper mFocusHandler;
+    private ViewGroupFocusHelper mFocusHandler;
 
     private RotationHelper mRotationHelper;
 
@@ -348,7 +349,7 @@
 
         LauncherAppState app = LauncherAppState.getInstance(this);
         mOldConfig = new Configuration(getResources().getConfiguration());
-        mModel = app.setLauncher(this);
+        mModel = app.getModel();
         mRotationHelper = new RotationHelper(this);
         InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
         initDeviceProfile(idp);
@@ -386,22 +387,18 @@
 
         // We only load the page synchronously if the user rotates (or triggers a
         // configuration change) while launcher is in the foreground
-        int currentScreen = PagedView.INVALID_RESTORE_PAGE;
+        int currentScreen = PagedView.INVALID_PAGE;
         if (savedInstanceState != null) {
             currentScreen = savedInstanceState.getInt(RUNTIME_STATE_CURRENT_SCREEN, currentScreen);
         }
+        mPageToBindSynchronously = currentScreen;
 
-        if (!mModel.startLoader(currentScreen)) {
+        if (!mModel.addCallbacksAndLoad(this)) {
             if (!internalStateHandled) {
                 // If we are not binding synchronously, show a fade in animation when
                 // the first page bind completes.
                 mDragLayer.getAlphaProperty(ALPHA_INDEX_LAUNCHER_LOAD).setValue(0);
             }
-        } else {
-            // Pages bound synchronously.
-            mWorkspace.setCurrentPage(currentScreen);
-
-            setWorkspaceLoading(true);
         }
 
         // For handling default keys
@@ -522,15 +519,6 @@
     }
 
     @Override
-    public void rebindModel() {
-        int currentPage = mWorkspace.getNextPage();
-        if (mModel.startLoader(currentPage)) {
-            mWorkspace.setCurrentPage(currentPage);
-            setWorkspaceLoading(true);
-        }
-    }
-
-    @Override
     public void onIdpChanged(int changeFlags, InvariantDeviceProfile idp) {
         onIdpChanged(idp);
     }
@@ -548,7 +536,7 @@
         // initialized properly.
         onSaveInstanceState(new Bundle());
         if (oldWallpaperProfile != getWallpaperDeviceProfile()) {
-            rebindModel();
+            mModel.rebindCallbacks();
         }
     }
 
@@ -617,6 +605,10 @@
         return mRotationHelper;
     }
 
+    public ViewGroupFocusHelper getFocusHandler() {
+        return mFocusHandler;
+    }
+
     public LauncherStateManager getStateManager() {
         return mStateManager;
     }
@@ -1539,13 +1531,7 @@
         mWorkspace.removeFolderListeners();
         PluginManagerWrapper.INSTANCE.get(this).removePluginListener(this);
 
-        // Stop callbacks from LauncherModel
-        // It's possible to receive onDestroy after a new Launcher activity has
-        // been created. In this case, don't interfere with the new Launcher.
-        if (mModel.isCurrentCallbacks(this)) {
-            mModel.stopLoader();
-            LauncherAppState.getInstance(this).setLauncher(null);
-        }
+        mModel.removeCallbacks(this);
         mRotationHelper.destroy();
 
         try {
@@ -1740,7 +1726,7 @@
         getModelWriter().addItemToDatabase(folderInfo, container, screenId, cellX, cellY);
 
         // Create the view
-        FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this, layout, folderInfo);
+        FolderIcon newFolder = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, this, layout, folderInfo);
         mWorkspace.addInScreen(newFolder, folderInfo);
         // Force measure the new folder icon
         CellLayout parent = mWorkspace.getParentCellLayoutForView(newFolder);
@@ -1953,11 +1939,21 @@
     }
 
     /**
+     * Sets the next page to bind synchronously on next bind.
+     * @param page
+     */
+    public void setPageToBindSynchronously(int page) {
+        mPageToBindSynchronously = page;
+    }
+
+    /**
      * Implementation of the method from LauncherModel.Callbacks.
      */
     @Override
-    public int getCurrentWorkspaceScreen() {
-        if (mWorkspace != null) {
+    public int getPageToBindSynchronously() {
+        if (mPageToBindSynchronously != PagedView.INVALID_PAGE) {
+            return mPageToBindSynchronously;
+        } else  if (mWorkspace != null) {
             return mWorkspace.getCurrentPage();
         } else {
             return 0;
@@ -2101,7 +2097,7 @@
                     break;
                 }
                 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
-                    view = FolderIcon.fromXml(R.layout.folder_icon, this,
+                    view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, this,
                             (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
                             (FolderInfo) item);
                     break;
@@ -2335,6 +2331,8 @@
 
     public void onPageBoundSynchronously(int page) {
         mSynchronouslyBoundPage = page;
+        mWorkspace.setCurrentPage(page);
+        mPageToBindSynchronously = PagedView.INVALID_PAGE;
     }
 
     @Override
@@ -2399,6 +2397,7 @@
         // Since we are just resetting the current page without user interaction,
         // override the previous page so we don't log the page switch.
         mWorkspace.setCurrentPage(pageBoundFirst, pageBoundFirst /* overridePrevPage */);
+        mPageToBindSynchronously = PagedView.INVALID_PAGE;
 
         // Cache one page worth of icons
         getViewCache().setCacheSize(R.layout.folder_application,
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index c6946ca..4cd038d 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -160,11 +160,6 @@
         }
     }
 
-    LauncherModel setLauncher(Launcher launcher) {
-        mModel.initialize(launcher);
-        return mModel;
-    }
-
     public IconCache getIconCache() {
         return mIconCache;
     }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index e005320..cf978b5 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -58,16 +58,16 @@
 import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.util.Thunk;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.Executor;
 import java.util.function.Supplier;
@@ -82,11 +82,12 @@
 
     static final String TAG = "Launcher.Model";
 
-    @Thunk final LauncherAppState mApp;
-    @Thunk final Object mLock = new Object();
-    @Thunk
-    LoaderTask mLoaderTask;
-    @Thunk boolean mIsLoaderTaskRunning;
+    private final LauncherAppState mApp;
+    private final Object mLock = new Object();
+    private final LooperExecutor mMainExecutor = MAIN_EXECUTOR;
+
+    private LoaderTask mLoaderTask;
+    private boolean mIsLoaderTaskRunning;
 
     // Indicates whether the current model data is valid or not.
     // We start off with everything not loaded. After that, we assume that
@@ -99,7 +100,7 @@
         }
     }
 
-    @Thunk WeakReference<Callbacks> mCallbacks;
+    private final ArrayList<Callbacks> mCallbacksList = new ArrayList<>(1);
 
     // < only access in worker thread >
     private final AllAppsList mBgAllAppsList;
@@ -127,12 +128,21 @@
     }
 
     /**
+     * Returns AppInfo with corresponding package name.
+     * TODO: move to enqueueModelTask
+     */
+    public Optional<AppInfo> getAppInfoByPackageName(String pkg) {
+        return mBgAllAppsList.data.stream()
+                .filter(info -> info.componentName.getPackageName().equals(pkg))
+                .findAny();
+    }
+
+    /**
      * Adds the provided items to the workspace.
      */
     public void addAndBindAddedWorkspaceItems(List<Pair<ItemInfo, Object>> itemList) {
-        Callbacks callbacks = getCallback();
-        if (callbacks != null) {
-            callbacks.preAddApps();
+        for (Callbacks cb : getCallbacks()) {
+            cb.preAddApps();
         }
         enqueueModelUpdateTask(new AddWorkspaceItemsTask(itemList));
     }
@@ -142,16 +152,6 @@
                 hasVerticalHotseat, verifyChanges);
     }
 
-    /**
-     * Set this as the current Launcher activity object for the loader.
-     */
-    public void initialize(Callbacks callbacks) {
-        synchronized (mLock) {
-            Preconditions.assertUIThread();
-            mCallbacks = new WeakReference<>(callbacks);
-        }
-    }
-
     @Override
     public void onPackageChanged(String packageName, UserHandle user) {
         int op = PackageUpdatedTask.OP_UPDATE;
@@ -251,21 +251,19 @@
                 }
             }
         } else if (IS_DOGFOOD_BUILD && ACTION_FORCE_ROLOAD.equals(action)) {
-            Launcher l = (Launcher) getCallback();
-            l.reload();
+            for (Callbacks cb : getCallbacks()) {
+                if (cb instanceof Launcher) {
+                    ((Launcher) cb).recreate();
+                }
+            }
         }
     }
 
-    public void forceReload() {
-        forceReload(-1);
-    }
-
     /**
      * Reloads the workspace items from the DB and re-binds the workspace. This should generally
      * not be called as DB updates are automatically followed by UI update
-     * @param synchronousBindPage The page to bind first. Can pass -1 to use the current page.
      */
-    public void forceReload(int synchronousBindPage) {
+    public void forceReload() {
         synchronized (mLock) {
             // Stop any existing loaders first, so they don't set mModelLoaded to true later
             stopLoader();
@@ -274,37 +272,77 @@
 
         // Start the loader if launcher is already running, otherwise the loader will run,
         // the next time launcher starts
-        Callbacks callbacks = getCallback();
-        if (callbacks != null) {
-            if (synchronousBindPage < 0) {
-                synchronousBindPage = callbacks.getCurrentWorkspaceScreen();
-            }
-            startLoader(synchronousBindPage);
+        if (hasCallbacks()) {
+            startLoader();
         }
     }
 
-    public boolean isCurrentCallbacks(Callbacks callbacks) {
-        return (mCallbacks != null && mCallbacks.get() == callbacks);
+    /**
+     * Rebinds all existing callbacks with already loaded model
+     */
+    public void rebindCallbacks() {
+        if (hasCallbacks()) {
+            startLoader();
+        }
+    }
+
+    /**
+     * Removes an existing callback
+     */
+    public void removeCallbacks(Callbacks callbacks) {
+        synchronized (mCallbacksList) {
+            Preconditions.assertUIThread();
+            if (mCallbacksList.remove(callbacks)) {
+                if (stopLoader()) {
+                    // Rebind existing callbacks
+                    startLoader();
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds a callbacks to receive model updates
+     * @return true if workspace load was performed synchronously
+     */
+    public boolean addCallbacksAndLoad(Callbacks callbacks) {
+        synchronized (mLock) {
+            addCallbacks(callbacks);
+            return startLoader();
+
+        }
+    }
+
+    /**
+     * Adds a callbacks to receive model updates
+     */
+    public void addCallbacks(Callbacks callbacks) {
+        Preconditions.assertUIThread();
+        synchronized (mCallbacksList) {
+            mCallbacksList.add(callbacks);
+        }
     }
 
     /**
      * Starts the loader. Tries to bind {@params synchronousBindPage} synchronously if possible.
      * @return true if the page could be bound synchronously.
      */
-    public boolean startLoader(int synchronousBindPage) {
+    public boolean startLoader() {
         // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
         InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_LOADER_RUNNING);
         synchronized (mLock) {
             // Don't bother to start the thread if we know it's not going to do anything
-            if (mCallbacks != null && mCallbacks.get() != null) {
-                final Callbacks oldCallbacks = mCallbacks.get();
+            final Callbacks[] callbacksList = getCallbacks();
+            if (callbacksList.length > 0) {
                 // Clear any pending bind-runnables from the synchronized load process.
-                MAIN_EXECUTOR.execute(oldCallbacks::clearPendingBinds);
+                for (Callbacks cb : callbacksList) {
+                    mMainExecutor.execute(cb::clearPendingBinds);
+                }
 
                 // If there is already one running, tell it to stop.
                 stopLoader();
-                LoaderResults loaderResults = new LoaderResults(mApp, mBgDataModel,
-                        mBgAllAppsList, synchronousBindPage, mCallbacks);
+                LoaderResults loaderResults = new LoaderResults(
+                        mApp, mBgDataModel, mBgAllAppsList, callbacksList, mMainExecutor);
                 if (mModelLoaded && !mIsLoaderTaskRunning) {
                     // Divide the set of loaded items into those that we are binding synchronously,
                     // and everything else that is to be bound normally (asynchronously).
@@ -325,14 +363,17 @@
 
     /**
      * If there is already a loader task running, tell it to stop.
+     * @return true if an existing loader was stopped.
      */
-    public void stopLoader() {
+    public boolean stopLoader() {
         synchronized (mLock) {
             LoaderTask oldTask = mLoaderTask;
             mLoaderTask = null;
             if (oldTask != null) {
                 oldTask.stopLocked();
+                return true;
             }
+            return false;
         }
     }
 
@@ -487,7 +528,7 @@
     }
 
     public void enqueueModelUpdateTask(ModelUpdateTask task) {
-        task.init(mApp, this, mBgDataModel, mBgAllAppsList, MAIN_EXECUTOR);
+        task.init(mApp, this, mBgDataModel, mBgAllAppsList, mMainExecutor);
         MODEL_EXECUTOR.execute(task);
     }
 
@@ -561,7 +602,21 @@
         mBgDataModel.dump(prefix, fd, writer, args);
     }
 
-    public Callbacks getCallback() {
-        return mCallbacks != null ? mCallbacks.get() : null;
+    /**
+     * Returns true if there are any callbacks attached to the model
+     */
+    public boolean hasCallbacks() {
+        synchronized (mCallbacksList) {
+            return !mCallbacksList.isEmpty();
+        }
+    }
+
+    /**
+     * Returns an array of currently attached callbacks
+     */
+    public Callbacks[] getCallbacks() {
+        synchronized (mCallbacksList) {
+            return mCallbacksList.toArray(new Callbacks[mCallbacksList.size()]);
+        }
     }
 }
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index ff2b400..a1888bf 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -64,7 +64,7 @@
     private static final String TAG = "PagedView";
     private static final boolean DEBUG = false;
 
-    protected static final int INVALID_PAGE = -1;
+    public static final int INVALID_PAGE = -1;
     protected static final ComputePageScrollsLogic SIMPLE_SCROLL_LOGIC = (v) -> v.getVisibility() != GONE;
 
     public static final int PAGE_SNAP_ANIMATION_DURATION = 750;
@@ -84,8 +84,6 @@
     private static final int MIN_SNAP_VELOCITY = 1500;
     private static final int MIN_FLING_VELOCITY = 250;
 
-    public static final int INVALID_RESTORE_PAGE = -1001;
-
     private boolean mFreeScroll = false;
 
     protected int mFlingThresholdVelocity;
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 7af979c..9a3a379 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -83,7 +83,6 @@
 import com.android.launcher3.pageindicators.WorkspacePageIndicator;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.WorkspaceTouchListener;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -418,9 +417,6 @@
         }
 
         // Always enter the spring loaded mode
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "Switching to SPRING_LOADED");
-        }
         mLauncher.getStateManager().goToState(SPRING_LOADED);
     }
 
@@ -1760,9 +1756,6 @@
     public void prepareAccessibilityDrop() { }
 
     public void onDrop(final DragObject d, DragOptions options) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "Workspace.onDrop");
-        }
         mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
         CellLayout dropTargetLayout = mDropToLayout;
 
@@ -2440,9 +2433,6 @@
      * to add an item to one of the workspace screens.
      */
     private void onDropExternal(final int[] touchXY, final CellLayout cellLayout, DragObject d) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "Workspace.onDropExternal");
-        }
         if (d.dragInfo instanceof PendingAddShortcutInfo) {
             WorkspaceItemInfo si = ((PendingAddShortcutInfo) d.dragInfo)
                     .activityInfo.createWorkspaceItemInfo();
@@ -2556,7 +2546,7 @@
                     view = mLauncher.createShortcut(cellLayout, (WorkspaceItemInfo) info);
                     break;
                 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
-                    view = FolderIcon.fromXml(R.layout.folder_icon, mLauncher, cellLayout,
+                    view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, mLauncher, cellLayout,
                             (FolderInfo) info);
                     break;
                 default:
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 08ce9c2..0681919 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -11,10 +11,12 @@
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
 import android.util.FloatProperty;
 import android.view.animation.Interpolator;
 
@@ -183,8 +185,11 @@
     }
 
     public Animator createSpringAnimation(float... progressValues) {
-        return new SpringObjectAnimator<>(this, ALL_APPS_PROGRESS, 1f / mShiftRange,
-                SPRING_DAMPING_RATIO, SPRING_STIFFNESS, progressValues);
+        if (UNSTABLE_SPRINGS.get()) {
+            return new SpringObjectAnimator<>(this, ALL_APPS_PROGRESS, 1f / mShiftRange,
+                    SPRING_DAMPING_RATIO, SPRING_STIFFNESS, progressValues);
+        }
+        return ObjectAnimator.ofFloat(this, ALL_APPS_PROGRESS, progressValues);
     }
 
     private void setAlphas(LauncherState toState, AnimationConfig config,
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 003ca82..81dcba3 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -85,7 +85,10 @@
             "APPLY_CONFIG_AT_RUNTIME", true, "Apply display changes dynamically");
 
     public static final TogglableFlag QUICKSTEP_SPRINGS = new TogglableFlag("QUICKSTEP_SPRINGS",
-            false, "Enable springs for quickstep animations");
+            true, "Enable springs for quickstep animations");
+
+    public static final TogglableFlag UNSTABLE_SPRINGS = new TogglableFlag("UNSTABLE_SPRINGS",
+            false, "Enable unstable springs for quickstep animations");
 
     public static final TogglableFlag ADAPTIVE_ICON_WINDOW_ANIM = new TogglableFlag(
             "ADAPTIVE_ICON_WINDOW_ANIM", true,
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index dcdf5d6..8adec27 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -594,9 +594,6 @@
     }
 
     private void drop(DropTarget dropTarget, Runnable flingAnimation) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "DragController.drop");
-        }
         final int[] coordinates = mCoordinatesTemp;
         mDragObject.x = coordinates[0];
         mDragObject.y = coordinates[1];
diff --git a/src/com/android/launcher3/dragndrop/DragDriver.java b/src/com/android/launcher3/dragndrop/DragDriver.java
index 01e0f92..87461d5 100644
--- a/src/com/android/launcher3/dragndrop/DragDriver.java
+++ b/src/com/android/launcher3/dragndrop/DragDriver.java
@@ -54,16 +54,10 @@
                 mEventListener.onDriverDragMove(ev.getX(), ev.getY());
                 break;
             case MotionEvent.ACTION_UP:
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "DragDriver.ACTION_UP");
-                }
                 mEventListener.onDriverDragMove(ev.getX(), ev.getY());
                 mEventListener.onDriverDragEnd(ev.getX(), ev.getY());
                 break;
             case MotionEvent.ACTION_CANCEL:
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE, "DragDriver.ACTION_CANCEL");
-                }
                 mEventListener.onDriverDragCancel();
                 break;
         }
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index f59a192..844189f 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -297,16 +297,22 @@
     }
 
     public void startEditingFolderName() {
-        post(new Runnable() {
-            @Override
-            public void run() {
-                mFolderName.setHint("");
-                mIsEditingName = true;
+        post(() -> {
+            if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+                if (TextUtils.isEmpty(mFolderName.getText())) {
+                    final String[] suggestedNames = new String[FolderNameProvider.SUGGEST_MAX];
+                    mLauncher.getFolderNameProvider().getSuggestedFolderName(getContext(),
+                            mInfo.contents, suggestedNames);
+                    mFolderName.setText(suggestedNames[0]);
+                    mFolderName.displayCompletions(Arrays.asList(suggestedNames).subList(1,
+                            suggestedNames.length));
+                }
             }
+            mFolderName.setHint("");
+            mIsEditingName = true;
         });
     }
 
-
     @Override
     public boolean onBackKey() {
         // Convert to a string here to ensure that no other state associated with the text field
@@ -316,10 +322,18 @@
         mFolderIcon.onTitleChanged(newTitle);
         mLauncher.getModelWriter().updateItemInDatabase(mInfo);
 
-        if (TextUtils.isEmpty(mInfo.title)) {
-            mFolderName.setHint(R.string.folder_hint_text);
+        if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+            mFolderName.setText(mInfo.title);
+            // TODO: depending on whether the title was manually edited or automatically
+            // suggested, apply different hint.
+            mFolderName.setHint("");
         } else {
-            mFolderName.setHint(null);
+            if (TextUtils.isEmpty(mInfo.title)) {
+                mFolderName.setHint(R.string.folder_hint_text);
+                mFolderName.setText("");
+            } else {
+                mFolderName.setHint(null);
+            }
         }
 
         sendCustomAccessibilityEvent(
@@ -403,7 +417,11 @@
             mFolderName.setHint(null);
         } else {
             mFolderName.setText("");
-            mFolderName.setHint(R.string.folder_hint_text);
+            if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+                mFolderName.setHint("");
+            } else {
+                mFolderName.setHint(R.string.folder_hint_text);
+            }
         }
         // In case any children didn't come across during loading, clean up the folder accordingly
         mFolderIcon.post(() -> {
@@ -420,10 +438,10 @@
         if (FeatureFlags.FOLDER_NAME_SUGGEST.get()
                 && TextUtils.isEmpty(mFolderName.getText().toString())) {
             if (suggestName.length > 0 && !TextUtils.isEmpty(suggestName[0])) {
-                mFolderName.setHint(suggestName[0]);
+                mFolderName.setHint("");
                 mFolderName.setText(suggestName[0]);
                 mInfo.title = suggestName[0];
-                animateOpen();
+                animateOpen(mInfo.contents, 0, true);
                 mFolderName.showKeyboard();
                 mFolderName.displayCompletions(
                         Arrays.asList(suggestName).subList(1, suggestName.length));
@@ -519,12 +537,24 @@
      * is played.
      */
     private void animateOpen(List<WorkspaceItemInfo> items, int pageNo) {
+        animateOpen(items, pageNo, false);
+    }
+
+    /**
+     * Opens the user folder described by the specified tag. The opening of the folder
+     * is animated relative to the specified View. If the View is null, no animation
+     * is played.
+     */
+    private void animateOpen(List<WorkspaceItemInfo> items, int pageNo, boolean skipUserEventLog) {
         Folder openFolder = getOpen(mLauncher);
         if (openFolder != null && openFolder != this) {
             // Close any open folder before opening a folder.
             openFolder.close(true);
         }
 
+        if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
+            mLauncher.getFolderNameProvider().load(getContext());
+        }
         mContent.bindItems(items);
         centerAboutIcon();
         mItemsInvalidated = true;
@@ -565,10 +595,13 @@
                 mState = STATE_OPEN;
                 announceAccessibilityChanges();
 
-                mLauncher.getUserEventDispatcher().logActionOnItem(
+                if (!skipUserEventLog) {
+                    mLauncher.getUserEventDispatcher().logActionOnItem(
                         Touch.TAP,
                         Direction.NONE,
                         ItemType.FOLDER_ICON, mInfo.cellX, mInfo.cellY);
+                }
+
 
                 mContent.setFocusOnFirstChild();
             }
@@ -1338,6 +1371,7 @@
         return itemsOnCurrentPage;
     }
 
+    @Override
     public void onFocusChange(View v, boolean hasFocus) {
         if (v == mFolderName) {
             if (hasFocus) {
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 7bbd45d..8c56823 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -67,6 +67,7 @@
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.IconLabelDotView;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 
@@ -79,7 +80,7 @@
  */
 public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView {
 
-    @Thunk Launcher mLauncher;
+    @Thunk ActivityContext mActivity;
     @Thunk Folder mFolder;
     private FolderInfo mInfo;
 
@@ -153,7 +154,21 @@
         mDotParams = new DotRenderer.DrawParams();
     }
 
-    public static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group,
+    public static FolderIcon inflateFolderAndIcon(int resId, Launcher launcher, ViewGroup group,
+            FolderInfo folderInfo) {
+        Folder folder = Folder.fromXml(launcher);
+        folder.setDragController(launcher.getDragController());
+
+        FolderIcon icon = inflateIcon(resId, launcher, group, folderInfo);
+        folder.setFolderIcon(icon);
+        folder.bind(folderInfo);
+        icon.setFolder(folder);
+
+        icon.setOnFocusChangeListener(launcher.getFocusHandler());
+        return icon;
+    }
+
+    public static FolderIcon inflateIcon(int resId, ActivityContext activity, ViewGroup group,
             FolderInfo folderInfo) {
         @SuppressWarnings("all") // suppress dead code warning
         final boolean error = INITIAL_ITEM_ANIMATION_DURATION >= DROP_IN_ANIMATION_DURATION;
@@ -163,7 +178,7 @@
                     "is dependent on this");
         }
 
-        DeviceProfile grid = launcher.getWallpaperDeviceProfile();
+        DeviceProfile grid = activity.getWallpaperDeviceProfile();
         FolderIcon icon = (FolderIcon) LayoutInflater.from(group.getContext())
                 .inflate(resId, group, false);
 
@@ -177,19 +192,27 @@
         icon.setTag(folderInfo);
         icon.setOnClickListener(ItemClickHandler.INSTANCE);
         icon.mInfo = folderInfo;
-        icon.mLauncher = launcher;
+        icon.mActivity = activity;
         icon.mDotRenderer = grid.mDotRendererWorkSpace;
-        icon.setContentDescription(launcher.getString(R.string.folder_name_format, folderInfo.title));
-        Folder folder = Folder.fromXml(launcher);
-        folder.setDragController(launcher.getDragController());
-        folder.setFolderIcon(icon);
-        folder.bind(folderInfo);
-        icon.setFolder(folder);
-        icon.setAccessibilityDelegate(launcher.getAccessibilityDelegate());
+
+        icon.setContentDescription(
+                group.getContext().getString(R.string.folder_name_format, folderInfo.title));
+
+        // Keep the notification dot up to date with the sum of all the content's dots.
+        FolderDotInfo folderDotInfo = new FolderDotInfo();
+        for (WorkspaceItemInfo si : folderInfo.contents) {
+            folderDotInfo.addDotInfo(activity.getDotInfoForItem(si));
+        }
+        icon.setDotInfo(folderDotInfo);
+
+        icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
+
+        icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile().inv);
+        icon.mPreviewVerifier.setFolderInfo(folderInfo);
+        icon.updatePreviewItems(false);
 
         folderInfo.addListener(icon);
 
-        icon.setOnFocusChangeListener(launcher.mFocusHandler);
         return icon;
     }
 
@@ -217,9 +240,6 @@
 
     private void setFolder(Folder folder) {
         mFolder = folder;
-        mPreviewVerifier = new FolderGridOrganizer(mLauncher.getDeviceProfile().inv);
-        mPreviewVerifier.setFolderInfo(mFolder.getInfo());
-        updatePreviewItems(false);
     }
 
     private boolean willAcceptItem(ItemInfo item) {
@@ -301,14 +321,15 @@
         // Typically, the animateView corresponds to the DragView; however, if this is being done
         // after a configuration activity (ie. for a Shortcut being dragged from AllApps) we
         // will not have a view to animate
-        if (animateView != null) {
-            DragLayer dragLayer = mLauncher.getDragLayer();
+        if (animateView != null && mActivity instanceof Launcher) {
+            final Launcher launcher = (Launcher) mActivity;
+            DragLayer dragLayer = launcher.getDragLayer();
             Rect from = new Rect();
             dragLayer.getViewRectRelativeToSelf(animateView, from);
             Rect to = finalRect;
             if (to == null) {
                 to = new Rect();
-                Workspace workspace = mLauncher.getWorkspace();
+                Workspace workspace = launcher.getWorkspace();
                 // Set cellLayout and this to it's final state to compute final animation locations
                 workspace.setFinalTransitionTransform();
                 float scaleX = getScaleX();
@@ -374,7 +395,7 @@
             String[] suggestedNameOut = new String[FolderNameProvider.SUGGEST_MAX];
             if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
                 Executors.UI_HELPER_EXECUTOR.post(() -> {
-                    mLauncher.getFolderNameProvider().getSuggestedFolderName(
+                    launcher.getFolderNameProvider().getSuggestedFolderName(
                             getContext(), mInfo.contents, suggestedNameOut);
                     showFinalView(finalIndex, item, suggestedNameOut);
                 });
@@ -539,7 +560,7 @@
         if (!mForceHideDot && ((mDotInfo != null && mDotInfo.hasDot()) || mDotScale > 0)) {
             Rect iconBounds = mDotParams.iconBounds;
             BubbleTextView.getIconBounds(this, iconBounds,
-                    mLauncher.getWallpaperDeviceProfile().iconSizePx);
+                    mActivity.getWallpaperDeviceProfile().iconSizePx);
             float iconScale = (float) mBackground.previewSize / iconBounds.width();
             Utilities.scaleRectAboutCenter(iconBounds, iconScale);
 
@@ -597,7 +618,7 @@
     @Override
     public void onAdd(WorkspaceItemInfo item, int rank) {
         boolean wasDotted = mDotInfo.hasDot();
-        mDotInfo.addDotInfo(mLauncher.getDotInfoForItem(item));
+        mDotInfo.addDotInfo(mActivity.getDotInfoForItem(item));
         boolean isDotted = mDotInfo.hasDot();
         updateDotScale(wasDotted, isDotted);
         invalidate();
@@ -607,7 +628,7 @@
     @Override
     public void onRemove(WorkspaceItemInfo item) {
         boolean wasDotted = mDotInfo.hasDot();
-        mDotInfo.subtractDotInfo(mLauncher.getDotInfoForItem(item));
+        mDotInfo.subtractDotInfo(mActivity.getDotInfoForItem(item));
         boolean isDotted = mDotInfo.hasDot();
         updateDotScale(wasDotted, isDotted);
         invalidate();
diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java
index 37aa815..d76b73f 100644
--- a/src/com/android/launcher3/folder/FolderNameProvider.java
+++ b/src/com/android/launcher3/folder/FolderNameProvider.java
@@ -15,53 +15,117 @@
  */
 package com.android.launcher3.folder;
 
-import android.content.ComponentName;
 import android.content.Context;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.Log;
 
-import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
 import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.config.FeatureFlags;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 /**
  * Locates provider for the folder name.
  */
 public class FolderNameProvider {
 
+    private static final String TAG = FeatureFlags.FOLDER_NAME_SUGGEST.getKey();
+    private static final boolean DEBUG = FeatureFlags.FOLDER_NAME_SUGGEST.get();
+
     /**
-     * IME usually has up to 3 suggest slots. Adding one as in Launcher, there are folder
-     * name edit box that we can also provide suggestion.
+     * IME usually has up to 3 suggest slots. In total, there are 4 suggest slots as the folder
+     * name edit box can also be used to provide suggestion.
      */
     public static final int SUGGEST_MAX = 4;
 
     /**
-     * Returns suggested folder name.
+     * When inheriting class requires precaching, override this method.
      */
-    public CharSequence getSuggestedFolderName(Context context,
-            ArrayList<WorkspaceItemInfo> workspaceItemInfos, CharSequence[] suggestName) {
-        // Currently only run the algorithm on initial folder creation.
-        // For more than 2 items in the folder, the ranking algorithm for finding
-        // candidate folder name should be rewritten.
-        if (workspaceItemInfos.size() == 2) {
-            ComponentName cmp1 = workspaceItemInfos.get(0).getTargetComponent();
-            ComponentName cmp2 = workspaceItemInfos.get(1).getTargetComponent();
+    public void load(Context context) {}
 
-            String pkgName0 = cmp1 == null ? "" : cmp1.getPackageName();
-            String pkgName1 = cmp2 == null ? "" : cmp2.getPackageName();
-            // If the two icons are from the same package,
-            // then assign the main icon's name
-            if (pkgName0.equals(pkgName1)) {
-                WorkspaceItemInfo wInfo0 = workspaceItemInfos.get(0);
-                WorkspaceItemInfo wInfo1 = workspaceItemInfos.get(1);
-                if (workspaceItemInfos.get(0).itemType == Favorites.ITEM_TYPE_APPLICATION) {
-                    suggestName[0] = wInfo0.title;
-                } else if (wInfo1.itemType == Favorites.ITEM_TYPE_APPLICATION) {
-                    suggestName[0] = wInfo1.title;
-                }
-                return suggestName[0];
-                // two icons are all shortcuts. Don't assign title
+    public CharSequence getSuggestedFolderName(Context context,
+            ArrayList<WorkspaceItemInfo> workspaceItemInfos, CharSequence[] candidates) {
+
+        if (DEBUG) {
+            Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(candidates));
+        }
+        // If all the icons are from work profile,
+        // Then, suggest "Work" as the folder name
+        List<WorkspaceItemInfo> distinctItemInfos = workspaceItemInfos.stream()
+                .filter(distinctByKey(p-> p.user))
+                .collect(Collectors.toList());
+
+        if (distinctItemInfos.size() == 1
+                && !distinctItemInfos.get(0).user.equals(Process.myUserHandle())) {
+            // Place it as last viable suggestion
+            setAsLastSuggestion(candidates,
+                    context.getResources().getString(R.string.work_folder_name));
+        }
+
+        // If all the icons are from same package (e.g., main icon, shortcut, shortcut)
+        // Then, suggest the package's title as the folder name
+        distinctItemInfos = workspaceItemInfos.stream()
+                .filter(distinctByKey(p-> p.getTargetComponent() != null
+                        ? p.getTargetComponent().getPackageName() : ""))
+                .collect(Collectors.toList());
+
+        if (distinctItemInfos.size() == 1) {
+            Optional<AppInfo> info = LauncherAppState.getInstance(context).getModel()
+                    .getAppInfoByPackageName(distinctItemInfos.get(0).getTargetComponent()
+                            .getPackageName());
+            // Place it as first viable suggestion and shift everything else
+            info.ifPresent(i -> setAsFirstSuggestion(candidates, i.title.toString()));
+        }
+        if (DEBUG) {
+            Log.d(TAG, "getSuggestedFolderName:" + Arrays.toString(candidates));
+        }
+        return candidates[0];
+    }
+
+    private void setAsFirstSuggestion(CharSequence[] candidatesOut, CharSequence candidate) {
+        if (contains(candidatesOut, candidate)) {
+            return;
+        }
+        for (int i = candidatesOut.length - 1; i > 0; i--) {
+            if (!TextUtils.isEmpty(candidatesOut[i - 1])) {
+                candidatesOut[i] = candidatesOut[i - 1];
             }
         }
-        return suggestName[0];
+        candidatesOut[0] = candidate;
+    }
+
+    private void setAsLastSuggestion(CharSequence[] candidatesOut, CharSequence candidate) {
+        if (contains(candidatesOut, candidate)) {
+            return;
+        }
+        for (int i = 0; i < candidate.length(); i++) {
+            if (TextUtils.isEmpty(candidatesOut[i])) {
+                candidatesOut[i] = candidate;
+            }
+        }
+    }
+
+    private boolean contains(CharSequence[] list, CharSequence key) {
+        return Arrays.asList(list).stream()
+                .filter(s -> s != null)
+                .anyMatch(s -> s.toString().equalsIgnoreCase(key.toString()));
+    }
+
+    // This method can be moved to some Utility class location.
+    private static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
+        Map<Object, Boolean> map = new ConcurrentHashMap<>();
+        return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
     }
 }
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 5b3a05e..27aa43e 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -37,10 +37,10 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.graphics.PreloadIconDrawable;
+import com.android.launcher3.views.ActivityContext;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -94,7 +94,8 @@
     public PreviewItemManager(FolderIcon icon) {
         mContext = icon.getContext();
         mIcon = icon;
-        mIconSize = Launcher.getLauncher(mContext).getDeviceProfile().folderChildIconSizePx;
+        mIconSize = ActivityContext.lookupContext(
+                mContext).getDeviceProfile().folderChildIconSizePx;
     }
 
     /**
@@ -132,7 +133,7 @@
             mTotalWidth = totalSize;
             mPrevTopPadding = mIcon.getPaddingTop();
 
-            mIcon.mBackground.setup(mIcon.mLauncher, mIcon.mLauncher, mIcon, mTotalWidth,
+            mIcon.mBackground.setup(mIcon.getContext(), mIcon.mActivity, mIcon, mTotalWidth,
                     mIcon.getPaddingTop());
             mIcon.mPreviewLayoutRule.init(mIcon.mBackground.previewSize, mIntrinsicIconSize,
                     Utilities.isRtl(mIcon.getResources()));
@@ -152,7 +153,7 @@
     }
 
     private PreviewItemDrawingParams getFinalIconParams(PreviewItemDrawingParams params) {
-        float iconSize = mIcon.mLauncher.getDeviceProfile().iconSizePx;
+        float iconSize = mIcon.mActivity.getDeviceProfile().iconSizePx;
 
         final float scale = iconSize / mReferenceDrawable.getIntrinsicWidth();
         final float trans = (mIcon.mBackground.previewSize - iconSize) / 2;
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 0c5535f..def76e8 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -49,6 +49,7 @@
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.FolderInfo;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.InvariantDeviceProfile;
@@ -63,11 +64,13 @@
 import com.android.launcher3.WorkspaceLayoutManager;
 import com.android.launcher3.allapps.SearchUiManager;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.icons.BaseIconFactory;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.BitmapRenderer;
 import com.android.launcher3.model.AllAppsList;
 import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.LoaderResults;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.BaseDragLayer;
@@ -239,6 +242,12 @@
             addInScreenFromBind(icon, info);
         }
 
+        private void inflateAndAddFolder(FolderInfo info) {
+            FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, mWorkspace,
+                    info);
+            addInScreenFromBind(folderIcon, info);
+        }
+
         private void dispatchVisibilityAggregated(View view, boolean isVisible) {
             // Similar to View.dispatchVisibilityAggregated implementation.
             final boolean thisVisible = view.getVisibility() == VISIBLE;
@@ -288,7 +297,7 @@
                             inflateAndAddIcon((WorkspaceItemInfo) itemInfo);
                             break;
                         case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
-                            // TODO: for folder implementation here.
+                            inflateAndAddFolder((FolderInfo) itemInfo);
                             break;
                         default:
                             break;
@@ -369,7 +378,7 @@
             if (!mModel.isModelLoaded()) {
                 Log.d(TAG, "Workspace not loaded, loading now");
                 mModel.startLoaderForResults(
-                        new LoaderResults(mApp, mBgDataModel, mAllAppsList, 0, null));
+                        new LoaderResults(mApp, mBgDataModel, mAllAppsList, new Callbacks[0]));
                 return new ArrayList<>();
             }
             return mBgDataModel.workspaceItems;
diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java
index 76c2951..0d12183 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -18,9 +18,7 @@
 
 import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
 import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
-import android.os.Looper;
 import android.util.Log;
 
 import com.android.launcher3.AppInfo;
@@ -32,12 +30,13 @@
 import com.android.launcher3.PagedView;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.LooperIdleLock;
 import com.android.launcher3.util.ViewOnDrawExecutor;
 
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
@@ -49,40 +48,29 @@
     protected static final int INVALID_SCREEN_ID = -1;
     private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons
 
-    protected final Executor mUiExecutor;
+    protected final LooperExecutor mUiExecutor;
 
     protected final LauncherAppState mApp;
     protected final BgDataModel mBgDataModel;
     private final AllAppsList mBgAllAppsList;
-    protected final int mPageToBindFirst;
 
-    protected final WeakReference<Callbacks> mCallbacks;
+    private final Callbacks[] mCallbacksList;
 
     private int mMyBindingId;
 
     public BaseLoaderResults(LauncherAppState app, BgDataModel dataModel,
-            AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
-        mUiExecutor = MAIN_EXECUTOR;
+            AllAppsList allAppsList, Callbacks[] callbacksList, LooperExecutor uiExecutor) {
+        mUiExecutor = uiExecutor;
         mApp = app;
         mBgDataModel = dataModel;
         mBgAllAppsList = allAppsList;
-        mPageToBindFirst = pageToBindFirst;
-        mCallbacks = callbacks == null ? new WeakReference<>(null) : callbacks;
+        mCallbacksList = callbacksList;
     }
 
     /**
      * Binds all loaded data to actual views on the main thread.
      */
     public void bindWorkspace() {
-        Callbacks callbacks = mCallbacks.get();
-        // Don't use these two variables in any of the callback runnables.
-        // Otherwise we hold a reference to them.
-        if (callbacks == null) {
-            // This launcher has exited and nobody bothered to tell us.  Just bail.
-            Log.w(TAG, "LoaderTask running with no launcher");
-            return;
-        }
-
         // Save a copy of all the bg-thread collections
         ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
         ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
@@ -96,97 +84,9 @@
             mMyBindingId = mBgDataModel.lastBindId;
         }
 
-        final int currentScreen;
-        {
-            int currScreen = mPageToBindFirst != PagedView.INVALID_RESTORE_PAGE
-                    ? mPageToBindFirst : callbacks.getCurrentWorkspaceScreen();
-            if (currScreen >= orderedScreenIds.size()) {
-                // There may be no workspace screens (just hotseat items and an empty page).
-                currScreen = PagedView.INVALID_RESTORE_PAGE;
-            }
-            currentScreen = currScreen;
-        }
-        final boolean validFirstPage = currentScreen >= 0;
-        final int currentScreenId =
-                validFirstPage ? orderedScreenIds.get(currentScreen) : INVALID_SCREEN_ID;
-
-        // Separate the items that are on the current screen, and all the other remaining items
-        ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
-        ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
-        ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
-        ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
-
-        filterCurrentWorkspaceItems(currentScreenId, workspaceItems, currentWorkspaceItems,
-                otherWorkspaceItems);
-        filterCurrentWorkspaceItems(currentScreenId, appWidgets, currentAppWidgets,
-                otherAppWidgets);
-        final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
-        sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
-        sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);
-
-        // Tell the workspace that we're about to start binding items
-        executeCallbacksTask(c -> {
-            c.clearPendingBinds();
-            c.startBinding();
-        }, mUiExecutor);
-
-        // Bind workspace screens
-        executeCallbacksTask(c -> c.bindScreens(orderedScreenIds), mUiExecutor);
-
-        Executor mainExecutor = mUiExecutor;
-        // Load items on the current page.
-        bindWorkspaceItems(currentWorkspaceItems, mainExecutor);
-        bindAppWidgets(currentAppWidgets, mainExecutor);
-        // In case of validFirstPage, only bind the first screen, and defer binding the
-        // remaining screens after first onDraw (and an optional the fade animation whichever
-        // happens later).
-        // This ensures that the first screen is immediately visible (eg. during rotation)
-        // In case of !validFirstPage, bind all pages one after other.
-        final Executor deferredExecutor =
-                validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;
-
-        executeCallbacksTask(c -> c.finishFirstPageBind(
-                validFirstPage ? (ViewOnDrawExecutor) deferredExecutor : null), mainExecutor);
-
-        bindWorkspaceItems(otherWorkspaceItems, deferredExecutor);
-        bindAppWidgets(otherAppWidgets, deferredExecutor);
-        // Tell the workspace that we're done binding items
-        executeCallbacksTask(c -> c.finishBindingItems(mPageToBindFirst), deferredExecutor);
-
-        if (validFirstPage) {
-            executeCallbacksTask(c -> {
-                // We are loading synchronously, which means, some of the pages will be
-                // bound after first draw. Inform the callbacks that page binding is
-                // not complete, and schedule the remaining pages.
-                if (currentScreen != PagedView.INVALID_RESTORE_PAGE) {
-                    c.onPageBoundSynchronously(currentScreen);
-                }
-                c.executeOnNextDraw((ViewOnDrawExecutor) deferredExecutor);
-
-            }, mUiExecutor);
-        }
-    }
-
-    protected void bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems,
-            final Executor executor) {
-        // Bind the workspace items
-        int N = workspaceItems.size();
-        for (int i = 0; i < N; i += ITEMS_CHUNK) {
-            final int start = i;
-            final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i);
-            executeCallbacksTask(
-                    c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
-                    executor);
-        }
-    }
-
-    private void bindAppWidgets(ArrayList<LauncherAppWidgetInfo> appWidgets, Executor executor) {
-        int N;// Bind the widgets, one at a time
-        N = appWidgets.size();
-        for (int i = 0; i < N; i++) {
-            final ItemInfo widget = appWidgets.get(i);
-            executeCallbacksTask(
-                    c -> c.bindItems(Collections.singletonList(widget), false), executor);
+        for (Callbacks cb : mCallbacksList) {
+            new WorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
+                    workspaceItems, appWidgets, orderedScreenIds).bind();
         }
     }
 
@@ -206,19 +106,155 @@
                 Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind");
                 return;
             }
-            Callbacks callbacks = mCallbacks.get();
-            if (callbacks != null) {
-                task.execute(callbacks);
+            for (Callbacks cb : mCallbacksList) {
+                task.execute(cb);
             }
         });
     }
 
     public LooperIdleLock newIdleLock(Object lock) {
-        LooperIdleLock idleLock = new LooperIdleLock(lock, Looper.getMainLooper());
+        LooperIdleLock idleLock = new LooperIdleLock(lock, mUiExecutor.getLooper());
         // If we are not binding or if the main looper is already idle, there is no reason to wait
-        if (mCallbacks.get() == null || Looper.getMainLooper().getQueue().isIdle()) {
+        if (mUiExecutor.getLooper().getQueue().isIdle()) {
             idleLock.queueIdle();
         }
         return idleLock;
     }
+
+    private static class WorkspaceBinder {
+
+        private final Executor mUiExecutor;
+        private final Callbacks mCallbacks;
+
+        private final LauncherAppState mApp;
+        private final BgDataModel mBgDataModel;
+
+        private final int mMyBindingId;
+        private final ArrayList<ItemInfo> mWorkspaceItems;
+        private final ArrayList<LauncherAppWidgetInfo> mAppWidgets;
+        private final IntArray mOrderedScreenIds;
+
+
+        WorkspaceBinder(Callbacks callbacks,
+                Executor uiExecutor,
+                LauncherAppState app,
+                BgDataModel bgDataModel,
+                int myBindingId,
+                ArrayList<ItemInfo> workspaceItems,
+                ArrayList<LauncherAppWidgetInfo> appWidgets,
+                IntArray orderedScreenIds) {
+            mCallbacks = callbacks;
+            mUiExecutor = uiExecutor;
+            mApp = app;
+            mBgDataModel = bgDataModel;
+            mMyBindingId = myBindingId;
+            mWorkspaceItems = workspaceItems;
+            mAppWidgets = appWidgets;
+            mOrderedScreenIds = orderedScreenIds;
+        }
+
+        private void bind() {
+            final int currentScreen;
+            {
+                // Create an anonymous scope to calculate currentScreen as it has to be a
+                // final variable.
+                int currScreen = mCallbacks.getPageToBindSynchronously();
+                if (currScreen >= mOrderedScreenIds.size()) {
+                    // There may be no workspace screens (just hotseat items and an empty page).
+                    currScreen = PagedView.INVALID_PAGE;
+                }
+                currentScreen = currScreen;
+            }
+            final boolean validFirstPage = currentScreen >= 0;
+            final int currentScreenId =
+                    validFirstPage ? mOrderedScreenIds.get(currentScreen) : INVALID_SCREEN_ID;
+
+            // Separate the items that are on the current screen, and all the other remaining items
+            ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
+            ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
+            ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
+            ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
+
+            filterCurrentWorkspaceItems(currentScreenId, mWorkspaceItems, currentWorkspaceItems,
+                    otherWorkspaceItems);
+            filterCurrentWorkspaceItems(currentScreenId, mAppWidgets, currentAppWidgets,
+                    otherAppWidgets);
+            final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
+            sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
+            sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);
+
+            // Tell the workspace that we're about to start binding items
+            executeCallbacksTask(c -> {
+                c.clearPendingBinds();
+                c.startBinding();
+            }, mUiExecutor);
+
+            // Bind workspace screens
+            executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
+
+            Executor mainExecutor = mUiExecutor;
+            // Load items on the current page.
+            bindWorkspaceItems(currentWorkspaceItems, mainExecutor);
+            bindAppWidgets(currentAppWidgets, mainExecutor);
+            // In case of validFirstPage, only bind the first screen, and defer binding the
+            // remaining screens after first onDraw (and an optional the fade animation whichever
+            // happens later).
+            // This ensures that the first screen is immediately visible (eg. during rotation)
+            // In case of !validFirstPage, bind all pages one after other.
+            final Executor deferredExecutor =
+                    validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;
+
+            executeCallbacksTask(c -> c.finishFirstPageBind(
+                    validFirstPage ? (ViewOnDrawExecutor) deferredExecutor : null), mainExecutor);
+
+            bindWorkspaceItems(otherWorkspaceItems, deferredExecutor);
+            bindAppWidgets(otherAppWidgets, deferredExecutor);
+            // Tell the workspace that we're done binding items
+            executeCallbacksTask(c -> c.finishBindingItems(currentScreen), deferredExecutor);
+
+            if (validFirstPage) {
+                executeCallbacksTask(c -> {
+                    // We are loading synchronously, which means, some of the pages will be
+                    // bound after first draw. Inform the mCallbacks that page binding is
+                    // not complete, and schedule the remaining pages.
+                    c.onPageBoundSynchronously(currentScreen);
+                    c.executeOnNextDraw((ViewOnDrawExecutor) deferredExecutor);
+
+                }, mUiExecutor);
+            }
+        }
+
+        private void bindWorkspaceItems(
+                final ArrayList<ItemInfo> workspaceItems, final Executor executor) {
+            // Bind the workspace items
+            int count = workspaceItems.size();
+            for (int i = 0; i < count; i += ITEMS_CHUNK) {
+                final int start = i;
+                final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i);
+                executeCallbacksTask(
+                        c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
+                        executor);
+            }
+        }
+
+        private void bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets, Executor executor) {
+            // Bind the widgets, one at a time
+            int count = appWidgets.size();
+            for (int i = 0; i < count; i++) {
+                final ItemInfo widget = appWidgets.get(i);
+                executeCallbacksTask(
+                        c -> c.bindItems(Collections.singletonList(widget), false), executor);
+            }
+        }
+
+        protected void executeCallbacksTask(CallbackTask task, Executor executor) {
+            executor.execute(() -> {
+                if (mMyBindingId != mBgDataModel.lastBindId) {
+                    Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind");
+                    return;
+                }
+                task.execute(mCallbacks);
+            });
+        }
+    }
 }
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index e12633b..5a7b4d3 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -20,17 +20,16 @@
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
-import com.android.launcher3.LauncherModel.ModelUpdateTask;
 import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.model.BgDataModel.Callbacks;
+import com.android.launcher3.LauncherModel.ModelUpdateTask;
 import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.widget.WidgetListRowEntry;
 
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
@@ -78,13 +77,9 @@
      * Schedules a {@param task} to be executed on the current callbacks.
      */
     public final void scheduleCallbackTask(final CallbackTask task) {
-        final Callbacks callbacks = mModel.getCallback();
-        mUiExecutor.execute(() -> {
-            Callbacks cb = mModel.getCallback();
-            if (callbacks == cb && cb != null) {
-                task.execute(callbacks);
-            }
-        });
+        for (final Callbacks cb : mModel.getCallbacks()) {
+            mUiExecutor.execute(() -> task.execute(cb));
+        }
     }
 
     public ModelWriter getModelWriter() {
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 88f2a09..c24b939 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -436,9 +436,10 @@
     }
 
     public interface Callbacks {
-        void rebindModel();
-
-        int getCurrentWorkspaceScreen();
+        /**
+         * Returns the page number to bind first, synchronously if possible or -1
+         */
+        int getPageToBindSynchronously();
         void clearPendingBinds();
         void startBinding();
         void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons);
diff --git a/src/com/android/launcher3/model/ModelPreload.java b/src/com/android/launcher3/model/ModelPreload.java
index 2bd6cd4..713492b 100644
--- a/src/com/android/launcher3/model/ModelPreload.java
+++ b/src/com/android/launcher3/model/ModelPreload.java
@@ -18,14 +18,15 @@
 import android.content.Context;
 import android.util.Log;
 
+import androidx.annotation.WorkerThread;
+
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 
 import java.util.concurrent.Executor;
 
-import androidx.annotation.WorkerThread;
-
 /**
  * Utility class to preload LauncherModel
  */
@@ -50,7 +51,7 @@
     @Override
     public final void run() {
         mModel.startLoaderForResultsIfNotLoaded(
-                new LoaderResults(mApp, mBgDataModel, mAllAppsList, 0, null));
+                new LoaderResults(mApp, mBgDataModel, mAllAppsList, new Callbacks[0]));
         Log.d(TAG, "Preload completed : " + mModel.isModelLoaded());
         onComplete(mModel.isModelLoaded());
     }
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index bdf3a69..ccd1554 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -41,7 +41,6 @@
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.ContentWriter;
 import com.android.launcher3.util.ItemInfoMatcher;
 
@@ -350,12 +349,15 @@
         mDeleteRunnables.clear();
     }
 
-    public void abortDelete(int pageToBindFirst) {
+    /**
+     * Aborts a previous delete operation pending commit
+     */
+    public void abortDelete() {
         mPreparingToUndo = false;
         mDeleteRunnables.clear();
         // We do a full reload here instead of just a rebind because Folders change their internal
         // state when dragging an item out, which clobbers the rebind unless we load from the DB.
-        mModel.forceReload(pageToBindFirst);
+        mModel.forceReload();
     }
 
     private class UpdateItemRunnable extends UpdateItemBaseRunnable {
@@ -472,7 +474,7 @@
         }
 
         void verifyModel() {
-            if (!mVerifyChanges || mModel.getCallback() == null) {
+            if (!mVerifyChanges || !mModel.hasCallbacks()) {
                 return;
             }
 
@@ -488,11 +490,9 @@
                     // Bound model has not changed during the job
                     return;
                 }
+
                 // Bound model was changed between submitting the job and executing the job
-                Callbacks callbacks = mModel.getCallback();
-                if (callbacks != null) {
-                    callbacks.rebindModel();
-                }
+                mModel.rebindCallbacks();
             });
         }
     }
diff --git a/src/com/android/launcher3/notification/NotificationFooterLayout.java b/src/com/android/launcher3/notification/NotificationFooterLayout.java
index c7de5b0..fd3d41a 100644
--- a/src/com/android/launcher3/notification/NotificationFooterLayout.java
+++ b/src/com/android/launcher3/notification/NotificationFooterLayout.java
@@ -80,17 +80,28 @@
         int iconSize = res.getDimensionPixelSize(R.dimen.notification_footer_icon_size);
         mIconLayoutParams = new LayoutParams(iconSize, iconSize);
         mIconLayoutParams.gravity = Gravity.CENTER_VERTICAL;
-        // Compute margin start for each icon such that the icons between the first one
-        // and the ellipsis are evenly spaced out.
+        setWidth((int) res.getDimension(R.dimen.bg_popup_item_width));
+        mBackgroundColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
+    }
+
+
+    /**
+     * Compute margin start for each icon such that the icons between the first one and the ellipsis
+     * are evenly spaced out.
+     */
+    public void setWidth(int width) {
+        if (getLayoutParams() != null) {
+            getLayoutParams().width = width;
+        }
+        Resources res = getResources();
+        int iconSize = res.getDimensionPixelSize(R.dimen.notification_footer_icon_size);
+
         int paddingEnd = res.getDimensionPixelSize(R.dimen.notification_footer_icon_row_padding);
         int ellipsisSpace = res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_offset)
                 + res.getDimensionPixelSize(R.dimen.horizontal_ellipsis_size);
-        int footerWidth = res.getDimensionPixelSize(R.dimen.bg_popup_item_width);
-        int availableIconRowSpace = footerWidth - paddingEnd - ellipsisSpace
+        int availableIconRowSpace = width - paddingEnd - ellipsisSpace
                 - iconSize * MAX_FOOTER_NOTIFICATIONS;
         mIconLayoutParams.setMarginStart(availableIconRowSpace / MAX_FOOTER_NOTIFICATIONS);
-
-        mBackgroundColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
     }
 
     @Override
diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java
index 021fb30..0320aa3 100644
--- a/src/com/android/launcher3/notification/NotificationItemView.java
+++ b/src/com/android/launcher3/notification/NotificationItemView.java
@@ -86,6 +86,13 @@
         }
     }
 
+    /**
+     * Sets width for notification footer and spaces out items evenly
+     */
+    public void setFooterWidth(int footerWidth) {
+        mFooter.setWidth(footerWidth);
+    }
+
     public void removeFooter() {
         if (mContainer.indexOfChild(mFooter) >= 0) {
             mContainer.removeView(mFooter);
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index e70673a..72c95c4 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -37,7 +37,6 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.util.Pair;
 import android.view.MotionEvent;
 import android.view.View;
@@ -66,7 +65,6 @@
 import com.android.launcher3.popup.PopupDataProvider.PopupDataChangeListener;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.util.PackageUserKey;
@@ -257,6 +255,16 @@
         mNumNotifications = notificationKeys.size();
         mOriginalIcon = originalIcon;
 
+        boolean hasDeepShortcuts = shortcutCount > 0;
+        int containerWidth = (int) getResources().getDimension(R.dimen.bg_popup_item_width);
+
+        // if there are deep shortcuts, we might want to increase the width of shortcuts to fit
+        // horizontally laid out system shortcuts.
+        if (hasDeepShortcuts) {
+            containerWidth = (int) Math.max(containerWidth,
+                    systemShortcuts.size() * getResources().getDimension(
+                            R.dimen.system_shortcut_header_icon_touch_size));
+        }
         // Add views
         if (mNumNotifications > 0) {
             // Add notification entries
@@ -265,18 +273,22 @@
             if (mNumNotifications == 1) {
                 mNotificationItemView.removeFooter();
             }
+            else {
+                mNotificationItemView.setFooterWidth(containerWidth);
+            }
             updateNotificationHeader();
         }
         int viewsToFlip = getChildCount();
         mSystemShortcutContainer = this;
-
-        if (shortcutCount > 0) {
+        if (hasDeepShortcuts) {
             if (mNotificationItemView != null) {
                 mNotificationItemView.addGutter();
             }
 
             for (int i = shortcutCount; i > 0; i--) {
-                mShortcuts.add(inflateAndAdd(R.layout.deep_shortcut, this));
+                DeepShortcutView v = inflateAndAdd(R.layout.deep_shortcut, this);
+                v.getLayoutParams().width = containerWidth;
+                mShortcuts.add(v);
             }
             updateHiddenShortcuts();
 
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index b580bd6..48f1c49 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -178,7 +178,10 @@
 
     public static final Factory<Launcher> DISMISS_PREDICTION = (launcher, itemInfo) -> {
         if (!FeatureFlags.ENABLE_PREDICTION_DISMISS.get()) return null;
-        if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_PREDICTION) return null;
+        if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_PREDICTION
+                && itemInfo.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+            return null;
+        }
         return new DismissPrediction(launcher, itemInfo);
     };
 
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index 36ff07e..9987994 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -114,9 +114,7 @@
         }
         try {
             int itemsDeleted = db.delete(Favorites.TABLE_NAME, whereClause.toString(), profileIds);
-            if (itemsDeleted > 0) {
-                FileLog.d(TAG, itemsDeleted + " items from unrestored user(s) were deleted");
-            }
+            FileLog.d(TAG, itemsDeleted + " items from unrestored user(s) were deleted");
         } catch (IllegalArgumentException exception) {
             // b/147114476
             FileLog.e(TAG, new StringBuilder("Failed to execute delete, where clause: '")
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 40e267b..ecfc77c 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -24,6 +24,7 @@
 import android.graphics.Color;
 import android.os.Bundle;
 import android.os.Debug;
+import android.system.Os;
 import android.view.View;
 
 import androidx.annotation.Keep;
@@ -136,6 +137,11 @@
                 break;
             }
 
+            case TestProtocol.REQUEST_PID: {
+                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD, Os.getpid());
+                break;
+            }
+
             case TestProtocol.REQUEST_TOTAL_PSS_KB: {
                 runGcAndFinalizersSync();
                 Debug.MemoryInfo mem = new Debug.MemoryInfo();
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 5aae841..929315a 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -73,6 +73,7 @@
     public static final String REQUEST_APPS_LIST_SCROLL_Y = "apps-list-scroll-y";
     public static final String REQUEST_OVERVIEW_LEFT_GESTURE_MARGIN = "overview-left-margin";
     public static final String REQUEST_OVERVIEW_RIGHT_GESTURE_MARGIN = "overview-right-margin";
+    public static final String REQUEST_PID = "pid";
     public static final String REQUEST_TOTAL_PSS_KB = "total_pss";
     public static final String REQUEST_JAVA_LEAK = "java-leak";
     public static final String REQUEST_NATIVE_LEAK = "native-leak";
@@ -86,6 +87,5 @@
     public static final String PERMANENT_DIAG_TAG = "TaplTarget";
 
     public static final String NO_BACKGROUND_TO_OVERVIEW_TAG = "b/138251824";
-    public static final String NO_DRAG_TO_WORKSPACE = "b/138729456";
     public static final String APP_NOT_DISABLED = "b/139891609";
 }
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index f40f976..f470edb 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -23,7 +23,7 @@
 import static com.android.launcher3.LauncherStateManager.ATOMIC_OVERVIEW_SCALE_COMPONENT;
 import static com.android.launcher3.LauncherStateManager.NON_ATOMIC_COMPONENT;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
 import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 
 import android.animation.Animator;
@@ -434,7 +434,7 @@
         updateSwipeCompleteAnimation(anim, Math.max(duration, getRemainingAtomicDuration()),
                 targetState, velocity, fling);
         mCurrentAnimation.dispatchOnStartWithVelocity(endProgress, velocity);
-        if (fling && targetState == LauncherState.ALL_APPS && !QUICKSTEP_SPRINGS.get()) {
+        if (fling && targetState == LauncherState.ALL_APPS && !UNSTABLE_SPRINGS.get()) {
             mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity);
         }
         anim.start();
diff --git a/src/com/android/launcher3/util/LogConfig.java b/src/com/android/launcher3/util/LogConfig.java
index 4acdb5c..b54074e 100644
--- a/src/com/android/launcher3/util/LogConfig.java
+++ b/src/com/android/launcher3/util/LogConfig.java
@@ -28,4 +28,9 @@
      * When turned on, icon cache is only fetched from memory and not disk.
      */
     public static final String MEMORY_ONLY_ICON_CACHE = "MemoryOnlyIconCache";
+
+    /**
+     * When turned on, we enable doodle related logging.
+     */
+    public static final String DOODLE_LOGGING = "DoodleLogging";
 }
diff --git a/src/com/android/launcher3/util/ViewOnDrawExecutor.java b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
index 5a131c8..451ae28 100644
--- a/src/com/android/launcher3/util/ViewOnDrawExecutor.java
+++ b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
@@ -23,6 +23,8 @@
 import android.view.View.OnAttachStateChangeListener;
 import android.view.ViewTreeObserver.OnDrawListener;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.Launcher;
 
 import java.util.ArrayList;
@@ -118,7 +120,11 @@
         return mCompleted;
     }
 
-    protected void runAllTasks() {
+    /**
+     * Executes all tasks immediately
+     */
+    @VisibleForTesting
+    public void runAllTasks() {
         for (final Runnable r : mTasks) {
             r.run();
         }
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index e43fc8a..cae2c3a 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -261,10 +261,6 @@
             }
             case ACTION_CANCEL:
             case ACTION_UP:
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.NO_DRAG_TO_WORKSPACE,
-                            "BaseDragLayer.ACTION_UP/CANCEL " + ev);
-                }
                 mTouchDispatchState &= ~TOUCH_DISPATCHING_GESTURE;
                 mTouchDispatchState &= ~TOUCH_DISPATCHING_VIEW;
                 break;
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 88d34da..5ba931d 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -47,7 +47,6 @@
 import java.util.ArrayList;
 import java.util.List;
 
-
 /**
  * Popup shown on long pressing an empty space in launcher
  */
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
index 789bfd8..dcb4636 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
@@ -16,12 +16,14 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.widget.WidgetListRowEntry;
 
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.HashMap;
 
@@ -31,8 +33,13 @@
 public class LoaderResults extends BaseLoaderResults {
 
     public LoaderResults(LauncherAppState app, BgDataModel dataModel,
-            AllAppsList allAppsList, int pageToBindFirst, WeakReference<Callbacks> callbacks) {
-        super(app, dataModel, allAppsList, pageToBindFirst, callbacks);
+            AllAppsList allAppsList, Callbacks[] callbacks) {
+        this(app, dataModel, allAppsList, callbacks, MAIN_EXECUTOR);
+    }
+
+    public LoaderResults(LauncherAppState app, BgDataModel dataModel,
+            AllAppsList allAppsList, Callbacks[] callbacks, LooperExecutor executor) {
+        super(app, dataModel, allAppsList, callbacks, executor);
     }
 
     @Override
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 56eca6d..1c8f095 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -22,6 +22,7 @@
 
     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
     <uses-permission android:name="android.permission.READ_LOGS"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
 
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner"/>
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index d7096b0..61f5150 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -18,6 +18,9 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.UNBUNDLED_POSTSUBMIT;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -36,10 +39,12 @@
 import com.android.launcher3.tapl.AppIconMenuItem;
 import com.android.launcher3.tapl.Widgets;
 import com.android.launcher3.tapl.Workspace;
+import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 import com.android.launcher3.views.OptionsPopupView;
 import com.android.launcher3.widget.WidgetsFullSheet;
 import com.android.launcher3.widget.WidgetsRecyclerView;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -50,10 +55,18 @@
 public class TaplTestsLauncher3 extends AbstractLauncherUiTest {
     private static final String APP_NAME = "LauncherTestApp";
 
+    private int mLauncherPid;
+
     @Before
     public void setUp() throws Exception {
         super.setUp();
         initialize(this);
+        mLauncherPid = mLauncher.getPid();
+    }
+
+    @After
+    public void teardown() {
+        assertEquals("Launcher crashed, pid mismatch:", mLauncherPid, mLauncher.getPid());
     }
 
     public static void initialize(AbstractLauncherUiTest test) throws Exception {
@@ -100,6 +113,16 @@
         mLauncher.pressHome();
     }
 
+    // b/146432215: remove @Stability after 2/1/2020 if this test doesn't flake
+    @Test
+    @Stability(flavors = LOCAL | UNBUNDLED_POSTSUBMIT)
+    public void testOpenHomeSettingsFromWorkspace() {
+        mDevice.pressMenu();
+        mDevice.waitForIdle();
+        mLauncher.getOptionsPopupMenu().getMenuItem("Home settings")
+                        .launch(mDevice.getLauncherPackageName());
+    }
+
     @Test
     @Ignore
     public void testPressHomeOnAllAppsContextMenu() throws Exception {
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 44fc3f7..2da6344 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -16,9 +16,6 @@
 
 package com.android.launcher3.tapl;
 
-import android.graphics.Point;
-import android.os.SystemClock;
-import android.view.MotionEvent;
 import android.widget.TextView;
 
 import androidx.test.uiautomator.By;
@@ -41,14 +38,8 @@
      * Long-clicks the icon to open its menu.
      */
     public AppIconMenu openMenu() {
-        final Point iconCenter = mObject.getVisibleCenter();
-        final long downTime = SystemClock.uptimeMillis();
-        mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, iconCenter);
-        final UiObject2 deepShortcutsContainer = mLauncher.waitForLauncherObject(
-                "deep_shortcuts_container");
-        mLauncher.sendPointer(
-                downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, iconCenter);
-        return new AppIconMenu(mLauncher, deepShortcutsContainer);
+        return new AppIconMenu(mLauncher, mLauncher.clickAndGet(
+                mObject, "deep_shortcuts_container"));
     }
 
     @Override
diff --git a/tests/tapl/com/android/launcher3/tapl/Folder.java b/tests/tapl/com/android/launcher3/tapl/Folder.java
deleted file mode 100644
index 6e6734d..0000000
--- a/tests/tapl/com/android/launcher3/tapl/Folder.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.tapl;
-
-import android.widget.FrameLayout;
-
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.BySelector;
-import androidx.test.uiautomator.UiObject2;
-
-/**
- * App folder in workspace/
- */
-public final class Folder {
-    Folder(LauncherInstrumentation launcher, UiObject2 icon) {
-    }
-
-    static BySelector getSelector(String folderName, LauncherInstrumentation launcher) {
-        return By.clazz(FrameLayout.class).desc(folderName).pkg(launcher.getLauncherPackageName());
-    }
-}
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index b715de0..95c4997 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -55,6 +55,7 @@
 import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.Configurator;
 import androidx.test.uiautomator.Direction;
+import androidx.test.uiautomator.StaleObjectException;
 import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.UiObject2;
 import androidx.test.uiautomator.Until;
@@ -183,13 +184,8 @@
                 .authority(testProviderAuthority)
                 .build();
 
-        try {
-            mDevice.executeShellCommand("pm grant " + testPackage +
-                    " android.permission.WRITE_SECURE_SETTINGS");
-        } catch (IOException e) {
-            fail(e.toString());
-        }
-
+        mInstrumentation.getUiAutomation().grantRuntimePermission(
+                testPackage, "android.permission.WRITE_SECURE_SETTINGS");
 
         PackageManager pm = getContext().getPackageManager();
         ProviderInfo pi = pm.resolveContentProvider(
@@ -260,9 +256,9 @@
 
     Closable addContextLayer(String piece) {
         mDiagnosticContext.addLast(piece);
-        log("Added context: " + getContextDescription());
+        log("Entering context: " + piece);
         return () -> {
-            log("Removing context: " + getContextDescription());
+            log("Leaving context: " + piece);
             mDiagnosticContext.removeLast();
         };
     }
@@ -311,12 +307,21 @@
     private String getVisiblePackages() {
         return mDevice.findObjects(By.textStartsWith(""))
                 .stream()
-                .map(object -> object.getApplicationPackage())
+                .map(LauncherInstrumentation::getApplicationPackageSafe)
                 .distinct()
-                .filter(pkg -> !"com.android.systemui".equals(pkg))
+                .filter(pkg -> pkg != null && !"com.android.systemui".equals(pkg))
                 .collect(Collectors.joining(", "));
     }
 
+    private static String getApplicationPackageSafe(UiObject2 object) {
+        try {
+            return object.getApplicationPackage();
+        } catch (StaleObjectException e) {
+            // We are looking at all object in the system; external ones can suddenly go away.
+            return null;
+        }
+    }
+
     private String getVisibleStateMessage() {
         if (hasLauncherObject(CONTEXT_MENU_RES_ID)) return "Context Menu";
         if (hasLauncherObject(WIDGETS_RES_ID)) return "Widgets";
@@ -336,14 +341,11 @@
 
     private String getSystemHealthMessage() {
         final String testPackage = getContext().getPackageName();
-        try {
-            mDevice.executeShellCommand("pm grant " + testPackage +
-                    " android.permission.READ_LOGS");
-            mDevice.executeShellCommand("pm grant " + testPackage +
-                    " android.permission.PACKAGE_USAGE_STATS");
-        } catch (IOException e) {
-            e.printStackTrace();
-        }
+
+        mInstrumentation.getUiAutomation().grantRuntimePermission(
+                testPackage, "android.permission.READ_LOGS");
+        mInstrumentation.getUiAutomation().grantRuntimePermission(
+                testPackage, "android.permission.PACKAGE_USAGE_STATS");
 
         return mSystemHealthSupplier != null
                 ? mSystemHealthSupplier.apply(START_TIME)
@@ -681,6 +683,20 @@
         }
     }
 
+    /**
+     * Gets the Options Popup Menu object if the current state is showing the popup menu. Fails if
+     * the launcher is not in that state.
+     *
+     * @return Options Popup Menu object.
+     */
+    @NonNull
+    public OptionsPopupMenu getOptionsPopupMenu() {
+        try (LauncherInstrumentation.Closable c = addContextLayer(
+                "want to get context menu object")) {
+            return new OptionsPopupMenu(this);
+        }
+    }
+
     void waitUntilGone(String resId) {
         assertTrue("Unexpected launcher object visible: " + resId,
                 mDevice.wait(Until.gone(getLauncherObjectSelector(resId)),
@@ -989,6 +1005,16 @@
         return getSystemIntegerRes(context, "config_navBarInteractionMode");
     }
 
+    @NonNull
+    UiObject2 clickAndGet(@NonNull final UiObject2 target, @NonNull String resName) {
+        final Point targetCenter = target.getVisibleCenter();
+        final long downTime = SystemClock.uptimeMillis();
+        sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter);
+        final UiObject2 result = waitForLauncherObject(resName);
+        sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter);
+        return result;
+    }
+
     private static int getSystemIntegerRes(Context context, String resName) {
         Resources res = context.getResources();
         int resId = res.getIdentifier(resName, "integer", "android");
@@ -1048,6 +1074,10 @@
                 getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    public int getPid() {
+        return getTestInfo(TestProtocol.REQUEST_PID).getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
+
     public void produceJavaLeak() {
         getTestInfo(TestProtocol.REQUEST_JAVA_LEAK);
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java
new file mode 100644
index 0000000..282fca9
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiObject2;
+
+public class OptionsPopupMenu {
+
+    private final LauncherInstrumentation mLauncher;
+    private final UiObject2 mDeepShortcutsContainer;
+
+    OptionsPopupMenu(LauncherInstrumentation launcher) {
+        mLauncher = launcher;
+        mDeepShortcutsContainer = launcher.waitForLauncherObject("deep_shortcuts_container");
+    }
+
+    /**
+     * Returns a menu item with a given label. Fails if it doesn't exist.
+     */
+    @NonNull
+    public OptionsPopupMenuItem getMenuItem(@NonNull final String label) {
+        final UiObject2 menuItem = mLauncher.waitForObjectInContainer(mDeepShortcutsContainer,
+                By.text(label));
+        return new OptionsPopupMenuItem(mLauncher, menuItem);
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
new file mode 100644
index 0000000..8527d05
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
+public class OptionsPopupMenuItem {
+
+    private final LauncherInstrumentation mLauncher;
+    private final UiObject2 mObject;
+
+    OptionsPopupMenuItem(@NonNull LauncherInstrumentation launcher, @NonNull UiObject2 shortcut) {
+        mLauncher = launcher;
+        mObject = shortcut;
+    }
+
+    /**
+     * Clicks the option.
+     */
+    @NonNull
+    public void launch(@NonNull String expectedPackageName) {
+        LauncherInstrumentation.log("OptionsPopupMenuItem before click "
+                + mObject.getVisibleCenter() + " in " + mObject.getVisibleBounds());
+        mObject.click();
+        mLauncher.assertTrue(
+                "App didn't start: " + By.pkg(expectedPackageName),
+                mLauncher.getDevice().wait(Until.hasObject(By.pkg(expectedPackageName)),
+                        LauncherInstrumentation.WAIT_TIME_MS));
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 3299d5d..af7e552 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -183,12 +183,6 @@
                 mHotseat, AppIcon.getAppIconSelector(appName, mLauncher)));
     }
 
-    @NonNull
-    public Folder getHotseatFolder(String appName) {
-        return new Folder(mLauncher, mLauncher.waitForObjectInContainer(
-                mHotseat, Folder.getSelector(appName, mLauncher)));
-    }
-
     static void dragIconToWorkspace(
             LauncherInstrumentation launcher, Launchable launchable, Point dest,
             String longPressIndicator) {
diff --git a/tools/checkstyle.xml b/tools/checkstyle.xml
new file mode 100644
index 0000000..0f4163d
--- /dev/null
+++ b/tools/checkstyle.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd" [
+  <!ENTITY defaultCopyrightCheck SYSTEM "../../../../prebuilts/checkstyle/default-copyright-check.xml">
+  <!ENTITY defaultJavadocChecks SYSTEM "../../../../prebuilts/checkstyle/default-javadoc-checks.xml">
+  <!ENTITY defaultTreewalkerChecks SYSTEM "../../../../prebuilts/checkstyle/default-treewalker-checks.xml">
+  <!ENTITY defaultModuleChecks SYSTEM "../../../../prebuilts/checkstyle/default-module-checks.xml">
+]>
+
+<module name="Checker">
+  &defaultModuleChecks;
+  &defaultCopyrightCheck;
+  <module name="TreeWalker">
+    &defaultJavadocChecks;
+    &defaultTreewalkerChecks;
+  </module>
+
+  <module name="SuppressionFilter">
+    <property name="file" value="tools/checkstyle_suppression.xml" />
+  </module>
+</module>
diff --git a/tools/checkstyle_suppression.xml b/tools/checkstyle_suppression.xml
new file mode 100644
index 0000000..799e750
--- /dev/null
+++ b/tools/checkstyle_suppression.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suppressions PUBLIC "-//Puppy Crawl//DTD Suppressions 1.1//EN" "http://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
+<suppressions>
+
+    <!-- Robolectric uses magic method names like `__constructor__` -->
+    <suppress files="/robolectric_tests" checks="MethodName|JavadocType|JavadocMethod" />
+
+</suppressions>
diff --git a/print_db.py b/tools/print_db.py
similarity index 100%
rename from print_db.py
rename to tools/print_db.py