Merge "Swipe up from nav bar in fallback recents to go home" into ub-launcher3-master
diff --git a/Android.mk b/Android.mk
index beb13c5..3a9a2c3 100644
--- a/Android.mk
+++ b/Android.mk
@@ -242,53 +242,7 @@
 LOCAL_PACKAGE_NAME := Launcher3QuickStepGo
 LOCAL_PRIVILEGED_MODULE := true
 LOCAL_SYSTEM_EXT_MODULE := true
-LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep Launcher3GoIconRecents
-LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
-
-LOCAL_FULL_LIBS_MANIFEST_FILES := \
-    $(LOCAL_PATH)/go/AndroidManifest.xml \
-    $(LOCAL_PATH)/quickstep/AndroidManifest-launcher.xml \
-    $(LOCAL_PATH)/AndroidManifest-common.xml
-
-LOCAL_MANIFEST_FILE := quickstep/AndroidManifest.xml
-LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.launcher3.*
-include $(BUILD_PACKAGE)
-
-#
-# Build rule for Launcher3 Go app with quickstep and Go-specific
-# version of recents for Android Go devices.
-#
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_STATIC_JAVA_LIBRARIES := SystemUISharedLibLauncherWrapper launcherprotosnano
-ifneq (,$(wildcard frameworks/base))
-  LOCAL_PRIVATE_PLATFORM_APIS := true
-else
-  LOCAL_SDK_VERSION := system_current
-  LOCAL_MIN_SDK_VERSION := 26
-endif
-LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3CommonDepsLib
-
-LOCAL_SRC_FILES := \
-    $(call all-java-files-under, src) \
-    $(call all-java-files-under, quickstep/src) \
-    $(call all-java-files-under, go/src) \
-    $(call all-java-files-under, go/quickstep/src)
-
-LOCAL_RESOURCE_DIR := \
-    $(LOCAL_PATH)/quickstep/res \
-    $(LOCAL_PATH)/go/res \
-    $(LOCAL_PATH)/go/quickstep/res
-
-LOCAL_PROGUARD_FLAG_FILES := proguard.flags
-LOCAL_PROGUARD_ENABLED := full
-
-LOCAL_PACKAGE_NAME := Launcher3GoIconRecents
-LOCAL_PRIVILEGED_MODULE := true
-LOCAL_SYSTEM_EXT_MODULE := true
-LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3Go Launcher3QuickStep
+LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3 Launcher3QuickStep
 LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
 
 LOCAL_FULL_LIBS_MANIFEST_FILES := \
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/quickstep/res/drawable/clear_all_button.xml b/go/quickstep/res/drawable/clear_all_button.xml
deleted file mode 100644
index acac32d..0000000
--- a/go/quickstep/res/drawable/clear_all_button.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-<shape
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <solid android:color="@color/clear_all_button_bg"/>
-    <corners android:radius="4dp"/>
-</shape>
diff --git a/go/quickstep/res/drawable/default_thumbnail.xml b/go/quickstep/res/drawable/default_thumbnail.xml
deleted file mode 100644
index ab22dcf..0000000
--- a/go/quickstep/res/drawable/default_thumbnail.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-<shape
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <solid android:color="@android:color/darker_gray"/>
-    <corners android:radius="@dimen/task_thumbnail_corner_radius"/>
-</shape>
diff --git a/go/quickstep/res/layout/clear_all_button.xml b/go/quickstep/res/layout/clear_all_button.xml
deleted file mode 100644
index eef66ad..0000000
--- a/go/quickstep/res/layout/clear_all_button.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/clear_all_item_view"
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/clear_all_item_view_height">
-    <Button
-        android:id="@+id/clear_all_button"
-        android:layout_width="@dimen/clear_all_button_width"
-        android:layout_height="match_parent"
-        android:layout_gravity="center_horizontal"
-        android:background="@drawable/clear_all_button"
-        android:gravity="center"
-        android:text="@string/recents_clear_all"
-        android:textAllCaps="false"
-        android:textColor="@color/clear_all_button_text"
-        android:textSize="14sp"
-        style="@style/TextTitle"/>
-</FrameLayout>
diff --git a/go/quickstep/res/layout/fallback_recents_activity.xml b/go/quickstep/res/layout/fallback_recents_activity.xml
deleted file mode 100644
index 653f463..0000000
--- a/go/quickstep/res/layout/fallback_recents_activity.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-     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.
--->
-<com.android.quickstep.fallback.GoRecentsActivityRootView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/drag_layer"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:fitsSystemWindows="true">
-
-    <include
-        android:id="@+id/overview_panel"
-        layout="@layout/overview_panel"
-        android:clipChildren="false"
-        android:clipToPadding="false"
-        android:outlineProvider="none"
-        android:theme="@style/HomeScreenElementTheme" />
-</com.android.quickstep.fallback.GoRecentsActivityRootView>
diff --git a/go/quickstep/res/layout/icon_recents_root_view.xml b/go/quickstep/res/layout/icon_recents_root_view.xml
deleted file mode 100644
index 8381ebc..0000000
--- a/go/quickstep/res/layout/icon_recents_root_view.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-<com.android.quickstep.views.IconRecentsView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical"
-    android:clipChildren="false">
-    <androidx.recyclerview.widget.RecyclerView
-        android:id="@+id/recent_task_recycler_view"
-        android:layout_width="@dimen/recents_list_width"
-        android:layout_height="match_parent"
-        android:layout_gravity="center_horizontal"
-        android:scrollbars="none"
-        android:clipToPadding="false"
-        android:clipChildren="false"/>
-    <TextView
-        android:id="@+id/recent_task_empty_view"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:gravity="center"
-        android:text="@string/recents_empty_message"
-        android:textColor="@android:color/white"
-        android:textSize="25sp"
-        style="@style/TextTitle"
-        android:visibility="gone"/>
-</com.android.quickstep.views.IconRecentsView>
\ No newline at end of file
diff --git a/go/quickstep/res/layout/overview_panel.xml b/go/quickstep/res/layout/overview_panel.xml
deleted file mode 100644
index 601edce..0000000
--- a/go/quickstep/res/layout/overview_panel.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-<fragment android:name="com.android.quickstep.IconRecentsFragment"
-          xmlns:android="http://schemas.android.com/apk/res/android"
-          android:id="@+id/low_ram_recents_fragment"
-          android:layout_width="match_parent"
-          android:layout_height="match_parent"/>
diff --git a/go/quickstep/res/layout/task_item_view.xml b/go/quickstep/res/layout/task_item_view.xml
deleted file mode 100644
index aeac477..0000000
--- a/go/quickstep/res/layout/task_item_view.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-<com.android.quickstep.views.TaskItemView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/task_item_height"
-    android:orientation="horizontal"
-    android:clipChildren="false">
-    <com.android.quickstep.views.TaskThumbnailIconView
-        android:id="@+id/task_icon_and_thumbnail"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_marginHorizontal="@dimen/task_thumbnail_icon_horiz_margin">
-        <ImageView
-            android:id="@+id/task_thumbnail"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"/>
-        <ImageView
-            android:id="@+id/task_icon"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"/>
-    </com.android.quickstep.views.TaskThumbnailIconView>
-    <TextView
-        android:id="@+id/task_label"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center_vertical"
-        android:singleLine="true"
-        android:textColor="@android:color/white"
-        android:textSize="24sp"
-        style="@style/TextTitle"/>
-</com.android.quickstep.views.TaskItemView>
diff --git a/go/quickstep/res/values-sw480dp/dimens.xml b/go/quickstep/res/values-sw480dp/dimens.xml
deleted file mode 100644
index 571b8a1..0000000
--- a/go/quickstep/res/values-sw480dp/dimens.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-<resources>
-    <dimen name="recents_list_width">480dp</dimen>
-
-    <dimen name="task_item_height">90dp</dimen>
-    <dimen name="task_item_top_margin">24dp</dimen>
-    <dimen name="task_thumbnail_icon_horiz_margin">24dp</dimen>
-
-    <dimen name="task_thumbnail_corner_radius">4dp</dimen>
-
-    <dimen name="clear_all_item_view_height">52dp</dimen>
-    <dimen name="clear_all_item_view_top_margin">28dp</dimen>
-    <dimen name="clear_all_item_view_bottom_margin">28dp</dimen>
-    <dimen name="clear_all_button_width">160dp</dimen>
-</resources>
\ No newline at end of file
diff --git a/go/quickstep/res/values/colors.xml b/go/quickstep/res/values/colors.xml
deleted file mode 100644
index ff9dc9c..0000000
--- a/go/quickstep/res/values/colors.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-<resources>
-    <color name="clear_all_button_bg">#FFDADCE0</color>
-    <color name="clear_all_button_text">#FF5F6368</color>
-</resources>
diff --git a/go/quickstep/res/values/dimens.xml b/go/quickstep/res/values/dimens.xml
deleted file mode 100644
index 91040f2..0000000
--- a/go/quickstep/res/values/dimens.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-<resources>
-    <dimen name="recents_list_width">320dp</dimen>
-
-    <dimen name="task_item_height">60dp</dimen>
-    <dimen name="task_item_top_margin">16dp</dimen>
-    <dimen name="task_thumbnail_icon_horiz_margin">16dp</dimen>
-
-    <dimen name="task_thumbnail_corner_radius">3dp</dimen>
-
-    <dimen name="clear_all_item_view_height">36dp</dimen>
-    <dimen name="clear_all_item_view_top_margin">20dp</dimen>
-    <dimen name="clear_all_item_view_bottom_margin">20dp</dimen>
-    <dimen name="clear_all_button_width">106dp</dimen>
-</resources>
\ No newline at end of file
diff --git a/go/quickstep/res/values/override.xml b/go/quickstep/res/values/override.xml
deleted file mode 100644
index bb267a3..0000000
--- a/go/quickstep/res/values/override.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2018 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<!-- Class overrides for Go version of launcher with Go recents. -->
-
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-  <string name="app_transition_manager_class" translatable="false">com.android.launcher3.GoLauncherAppTransitionManagerImpl</string>
-
-  <string name="instant_app_resolver_class" translatable="false">com.android.quickstep.InstantAppResolverImpl</string>
-
-  <string name="main_process_initializer_class" translatable="false">com.android.quickstep.QuickstepProcessInitializer</string>
-
-  <string name="user_event_dispatcher_class" translatable="false">com.android.quickstep.logging.UserEventDispatcherExtension</string>
-</resources>
-
diff --git a/go/quickstep/src/com/android/launcher3/GoLauncherAppTransitionManagerImpl.java b/go/quickstep/src/com/android/launcher3/GoLauncherAppTransitionManagerImpl.java
deleted file mode 100644
index 3953fd0..0000000
--- a/go/quickstep/src/com/android/launcher3/GoLauncherAppTransitionManagerImpl.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package com.android.launcher3;
-
-import static com.android.launcher3.Utilities.postAsyncCallback;
-import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
-import static com.android.quickstep.views.IconRecentsView.CONTENT_ALPHA;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
-
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.os.Handler;
-import android.view.View;
-
-import com.android.quickstep.views.IconRecentsView;
-import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-/**
- * A {@link QuickstepAppTransitionManagerImpl} with recents-specific app transitions based off
- * {@link com.android.quickstep.views.IconRecentsView}.
- */
-public final class GoLauncherAppTransitionManagerImpl extends QuickstepAppTransitionManagerImpl {
-
-    public GoLauncherAppTransitionManagerImpl(Context context) {
-        super(context);
-    }
-
-    @Override
-    protected boolean isLaunchingFromRecents(View v, RemoteAnimationTargetCompat[] targets) {
-        return mLauncher.getStateManager().getState().overviewUi;
-    }
-
-    @Override
-    RemoteAnimationRunnerCompat getWallpaperOpenRunner(boolean fromUnlock) {
-        return new GoWallpaperOpenLauncherAnimationRunner(mHandler,
-                false /* startAtFrontOfQueue */, fromUnlock);
-    }
-
-    @Override
-    protected void composeRecentsLaunchAnimator(AnimatorSet anim, View v,
-            RemoteAnimationTargetCompat[] appTargets,
-            RemoteAnimationTargetCompat[] wallpaperTargets,
-            boolean launcherClosing) {
-        // Stubbed. Recents launch animation will come from the recents view itself and will not
-        // use remote animations.
-    }
-
-    @Override
-    protected Runnable composeViewContentAnimator(AnimatorSet anim, float[] alphas, float[] trans) {
-        IconRecentsView overview = mLauncher.getOverviewPanel();
-        ObjectAnimator alpha = ObjectAnimator.ofFloat(overview,
-                CONTENT_ALPHA, alphas);
-        alpha.setDuration(CONTENT_ALPHA_DURATION);
-        alpha.setInterpolator(LINEAR);
-        anim.play(alpha);
-
-        ObjectAnimator transY = ObjectAnimator.ofFloat(overview, View.TRANSLATION_Y, trans);
-        transY.setInterpolator(AGGRESSIVE_EASE);
-        transY.setDuration(CONTENT_TRANSLATION_DURATION);
-        anim.play(transY);
-
-        return mLauncher.getStateManager()::reapplyState;
-    }
-
-    /**
-     * Remote animation runner for animation from app to Launcher. For Go, when going to recents,
-     * we need to ensure that the recents view is ready for remote animation before starting.
-     */
-    private final class GoWallpaperOpenLauncherAnimationRunner extends
-            WallpaperOpenLauncherAnimationRunner {
-        public GoWallpaperOpenLauncherAnimationRunner(Handler handler, boolean startAtFrontOfQueue,
-                boolean fromUnlock) {
-            super(handler, startAtFrontOfQueue, fromUnlock);
-        }
-
-        @Override
-        public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
-                RemoteAnimationTargetCompat[] wallpaperTargets,
-                AnimationResult result) {
-            boolean isGoingToRecents =
-                    taskIsATargetWithMode(appTargets, mLauncher.getTaskId(), MODE_OPENING)
-                    && (mLauncher.getStateManager().getState() == LauncherState.OVERVIEW);
-            if (isGoingToRecents) {
-                IconRecentsView recentsView = mLauncher.getOverviewPanel();
-                if (!recentsView.isReadyForRemoteAnim()) {
-                    recentsView.setOnReadyForRemoteAnimCallback(() ->
-                        postAsyncCallback(mHandler, () -> onCreateAnimation(appTargets,
-                                wallpaperTargets, result))
-                    );
-                    return;
-                }
-            }
-            super.onCreateAnimation(appTargets, wallpaperTargets, result);
-        }
-    }
-}
diff --git a/go/quickstep/src/com/android/launcher3/LauncherRecentsToActivityHelper.java b/go/quickstep/src/com/android/launcher3/LauncherRecentsToActivityHelper.java
deleted file mode 100644
index c12da94..0000000
--- a/go/quickstep/src/com/android/launcher3/LauncherRecentsToActivityHelper.java
+++ /dev/null
@@ -1,38 +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;
-
-import static com.android.launcher3.LauncherState.NORMAL;
-
-import com.android.quickstep.RecentsToActivityHelper;
-
-/**
- * {@link RecentsToActivityHelper} for when the recents implementation is contained in
- * {@link Launcher}.
- */
-public final class LauncherRecentsToActivityHelper implements RecentsToActivityHelper {
-
-    private final Launcher mLauncher;
-
-    public LauncherRecentsToActivityHelper(Launcher launcher) {
-        mLauncher = launcher;
-    }
-
-    @Override
-    public void leaveRecents() {
-        mLauncher.getStateManager().goToState(NORMAL);
-    }
-}
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/go/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
deleted file mode 100644
index 0c60468..0000000
--- a/go/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ /dev/null
@@ -1,46 +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.uioverrides;
-
-import com.android.launcher3.BaseQuickstepLauncher;
-import com.android.launcher3.uioverrides.touchcontrollers.LandscapeEdgeSwipeController;
-import com.android.launcher3.uioverrides.touchcontrollers.LandscapeStatesTouchController;
-import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController;
-import com.android.launcher3.util.TouchController;
-import com.android.quickstep.SysUINavigationMode;
-
-import java.util.ArrayList;
-
-public class QuickstepLauncher extends BaseQuickstepLauncher {
-
-    public static final boolean GO_LOW_RAM_RECENTS_ENABLED = true;
-
-    @Override
-    public TouchController[] createTouchControllers() {
-        ArrayList<TouchController> list = new ArrayList<>();
-        list.add(getDragController());
-
-        if (getDeviceProfile().isVerticalBarLayout()) {
-            list.add(new LandscapeStatesTouchController(this));
-            list.add(new LandscapeEdgeSwipeController(this));
-        } else {
-            boolean allowDragToOverview = SysUINavigationMode.INSTANCE.get(this)
-                    .getMode().hasGestures;
-            list.add(new PortraitStatesTouchController(this, allowDragToOverview));
-        }
-        return list.toArray(new TouchController[list.size()]);
-    }
-}
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/go/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
deleted file mode 100644
index 0b12ab0..0000000
--- a/go/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ /dev/null
@@ -1,44 +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.uioverrides;
-
-import static com.android.quickstep.views.IconRecentsView.CONTENT_ALPHA;
-
-import android.util.FloatProperty;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherRecentsToActivityHelper;
-import com.android.quickstep.views.IconRecentsView;
-
-import androidx.annotation.NonNull;
-
-/**
- * State handler for Go's {@link IconRecentsView}.
- */
-public final class RecentsViewStateController extends
-        BaseRecentsViewStateController<IconRecentsView> {
-
-    public RecentsViewStateController(@NonNull Launcher launcher) {
-        super(launcher);
-        launcher.<IconRecentsView>getOverviewPanel().setRecentsToActivityHelper(
-                new LauncherRecentsToActivityHelper(launcher));
-    }
-
-    @Override
-    FloatProperty<IconRecentsView> getContentAlphaProperty() {
-        return CONTENT_ALPHA;
-    }
-}
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
deleted file mode 100644
index 212ce9b..0000000
--- a/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ /dev/null
@@ -1,159 +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.uioverrides.states;
-
-import static android.view.View.VISIBLE;
-
-import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
-import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATE_X;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_FADE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_SCALE;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_WORKSPACE_TRANSLATE;
-import static com.android.launcher3.anim.Interpolators.ACCEL;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
-import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7;
-import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
-
-import android.content.Context;
-import android.view.View;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.AnimatorSetBuilder;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.SysUINavigationMode;
-import com.android.quickstep.views.IconRecentsView;
-
-/**
- * Definition for overview state
- */
-public class OverviewState extends LauncherState {
-
-    // Scale recents takes before animating in
-    private static final float RECENTS_PREPARE_SCALE = 1.33f;
-
-    private static final int STATE_FLAGS = FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED
-            | FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_DISABLE_ACCESSIBILITY;
-
-    public OverviewState(int id) {
-        this(id, OVERVIEW_TRANSITION_MS, STATE_FLAGS);
-    }
-
-    protected OverviewState(int id, int transitionDuration, int stateFlags) {
-        super(id, LauncherLogProto.ContainerType.TASKSWITCHER, transitionDuration, stateFlags);
-    }
-
-    @Override
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        return new ScaleAndTranslation(1f, 0f, 0f);
-    }
-
-    @Override
-    public void onStateEnabled(Launcher launcher) {
-        IconRecentsView recentsView = launcher.getOverviewPanel();
-        recentsView.onBeginTransitionToOverview();
-        recentsView.setShowStatusBarForegroundScrim(true);
-        // Request orientation be set to unspecified, letting the system decide the best
-        // orientation.
-        launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE);
-    }
-
-    @Override
-    public void onStateDisabled(Launcher launcher) {
-        IconRecentsView recentsView = launcher.getOverviewPanel();
-        recentsView.setShowStatusBarForegroundScrim(false);
-    }
-
-    @Override
-    public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
-        return new PageAlphaProvider(DEACCEL_2) {
-            @Override
-            public float getPageAlpha(int pageIndex) {
-                return 0;
-            }
-        };
-    }
-
-    @Override
-    public int getVisibleElements(Launcher launcher) {
-        return NONE;
-    }
-
-    @Override
-    public float getWorkspaceScrimAlpha(Launcher launcher) {
-        return 0.5f;
-    }
-
-    @Override
-    public String getDescription(Launcher launcher) {
-        return launcher.getString(R.string.accessibility_desc_recent_apps);
-    }
-
-    @Override
-    public void onBackPressed(Launcher launcher) {
-        // TODO: Add logic to go back to task if coming from a currently running task.
-        super.onBackPressed(launcher);
-    }
-
-    public static float getDefaultSwipeHeight(Launcher launcher) {
-        return getDefaultSwipeHeight(launcher, launcher.getDeviceProfile());
-    }
-
-    public static float getDefaultSwipeHeight(Context context, DeviceProfile dp) {
-        return dp.allAppsCellHeightPx - dp.allAppsIconTextSizePx;
-    }
-
-    @Override
-    public void prepareForAtomicAnimation(Launcher launcher, LauncherState fromState,
-            AnimatorSetBuilder builder) {
-        if (fromState == NORMAL && this == OVERVIEW) {
-            if (SysUINavigationMode.getMode(launcher) == SysUINavigationMode.Mode.NO_BUTTON) {
-                builder.setInterpolator(ANIM_WORKSPACE_SCALE, ACCEL);
-                builder.setInterpolator(ANIM_WORKSPACE_TRANSLATE, ACCEL);
-            } else {
-                builder.setInterpolator(ANIM_WORKSPACE_SCALE, OVERSHOOT_1_2);
-            }
-            builder.setInterpolator(ANIM_WORKSPACE_FADE, OVERSHOOT_1_2);
-            builder.setInterpolator(ANIM_OVERVIEW_SCALE, OVERSHOOT_1_2);
-            builder.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_7);
-            builder.setInterpolator(ANIM_OVERVIEW_FADE, OVERSHOOT_1_2);
-
-            View overview = launcher.getOverviewPanel();
-            if (overview.getVisibility() != VISIBLE) {
-                SCALE_PROPERTY.set(overview, RECENTS_PREPARE_SCALE);
-            }
-        }
-    }
-
-    public static OverviewState newBackgroundState(int id) {
-        return new OverviewState(id);
-    }
-
-    public static OverviewState newPeekState(int id) {
-        return new OverviewState(id);
-    }
-
-    public static OverviewState newSwitchState(int id) {
-        return new OverviewState(id);
-    }
-}
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeStatesTouchController.java b/go/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeStatesTouchController.java
deleted file mode 100644
index 66aec40..0000000
--- a/go/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/LandscapeStatesTouchController.java
+++ /dev/null
@@ -1,70 +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.uioverrides.touchcontrollers;
-
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.NORMAL;
-
-import android.view.MotionEvent;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-
-/**
- * Touch controller for landscape mode.
- */
-public final class LandscapeStatesTouchController extends PortraitStatesTouchController {
-
-    public LandscapeStatesTouchController(Launcher l) {
-        super(l, true /* allowDragToOverview */);
-    }
-
-    @Override
-    protected boolean canInterceptTouch(MotionEvent ev) {
-        if (mCurrentAnimation != null) {
-            // If we are already animating from a previous state, we can intercept.
-            return true;
-        }
-        if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
-            return false;
-        }
-        if (mLauncher.isInState(ALL_APPS)) {
-            // In all-apps only listen if the container cannot scroll itself
-            return mLauncher.getAppsView().shouldContainerScroll(ev);
-        } else if (mLauncher.isInState(NORMAL)) {
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    @Override
-    protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
-        if (fromState == ALL_APPS && !isDragTowardPositive) {
-            return NORMAL;
-        } else if (isDragTowardPositive) {
-            return ALL_APPS;
-        }
-        return fromState;
-    }
-
-    @Override
-    protected int getLogContainerTypeForNormalState(MotionEvent ev) {
-        return LauncherLogProto.ContainerType.WORKSPACE;
-    }
-}
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java b/go/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
deleted file mode 100644
index 011a4e7..0000000
--- a/go/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
+++ /dev/null
@@ -1,65 +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.uioverrides.touchcontrollers;
-
-import android.view.MotionEvent;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.util.PendingAnimation;
-
-/**
- * Helper class for {@link PortraitStatesTouchController} that determines swipeable regions and
- * animations on the overview state that depend on the recents implementation.
- */
-public final class PortraitOverviewStateTouchHelper {
-
-    public PortraitOverviewStateTouchHelper(Launcher launcher) {}
-
-    /**
-     * Whether or not {@link PortraitStatesTouchController} should intercept the touch when on the
-     * overview state.
-     *
-     * @param ev the motion event
-     * @return true if we should intercept the motion event
-     */
-    boolean canInterceptTouch(MotionEvent ev) {
-        // Go does not support swiping to all-apps from recents.
-        return false;
-    }
-
-    /**
-     * Whether or not swiping down to leave overview state should return to the currently running
-     * task app.
-     *
-     * @return true if going back should take the user to the currently running task
-     */
-    boolean shouldSwipeDownReturnToApp() {
-        // Go does not support swiping tasks down to launch tasks from recents.
-        return false;
-    }
-
-    /**
-     * Create the animation for going from overview to the task app via swiping.
-     *
-     * @param duration how long the animation should be
-     * @return the animation
-     */
-    PendingAnimation createSwipeDownToTaskAppAnimation(long duration) {
-        // Go does not support swiping tasks down to launch tasks from recents.
-        return null;
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
deleted file mode 100644
index 04753d2..0000000
--- a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.Utilities.postAsyncCallback;
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
-import static com.android.quickstep.views.IconRecentsView.REMOTE_APP_TO_OVERVIEW_DURATION;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
-
-import android.animation.AnimatorSet;
-import android.animation.ValueAnimator;
-import android.app.ActivityOptions;
-import android.content.Context;
-import android.os.Handler;
-import android.util.Log;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.LauncherAnimationRunner;
-import com.android.quickstep.util.RemoteAnimationProvider;
-import com.android.quickstep.views.IconRecentsView;
-import com.android.systemui.shared.system.ActivityOptionsCompat;
-import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-/**
- * Provider for the atomic remote window animation from the app to the overview.
- *
- * @param <T> activity that contains the overview
- */
-final class AppToOverviewAnimationProvider<T extends BaseDraggingActivity> implements
-        RemoteAnimationProvider {
-    private static final String TAG = "AppToOverviewAnimationProvider";
-
-    private final BaseActivityInterface<T> mActivityInterface;
-    private final int mTargetTaskId;
-    private IconRecentsView mRecentsView;
-    private AppToOverviewAnimationListener mAnimationReadyListener;
-
-    AppToOverviewAnimationProvider(BaseActivityInterface<T> activityInterface, int targetTaskId) {
-        mActivityInterface = activityInterface;
-        mTargetTaskId = targetTaskId;
-    }
-
-    /**
-     * Set listener to various points in the animation preparing to animate.
-     *
-     * @param listener listener
-     */
-    void setAnimationListener(AppToOverviewAnimationListener listener) {
-        mAnimationReadyListener = listener;
-    }
-
-    /**
-     * Callback for when the activity is ready/initialized.
-     *
-     * @param wasVisible true if it was visible before
-     */
-    boolean onActivityReady(Boolean wasVisible) {
-        T activity = mActivityInterface.getCreatedActivity();
-        if (mAnimationReadyListener != null) {
-            mAnimationReadyListener.onActivityReady(activity);
-        }
-        BaseActivityInterface.AnimationFactory factory =
-                mActivityInterface.prepareRecentsUI(wasVisible,
-                        false /* animate activity */, (controller) -> {
-                            controller.dispatchOnStart();
-                            ValueAnimator anim = controller.getAnimationPlayer()
-                                    .setDuration(getRecentsLaunchDuration());
-                            anim.setInterpolator(FAST_OUT_SLOW_IN);
-                            anim.start();
-                        });
-        factory.onRemoteAnimationReceived(null);
-        factory.createActivityInterface(getRecentsLaunchDuration());
-        mRecentsView = activity.getOverviewPanel();
-        return false;
-    }
-
-    /**
-     * Create remote window animation from the currently running app to the overview panel. Should
-     * be called after {@link #onActivityReady}.
-     *
-     * @param appTargets the target apps
-     * @return animation from app to overview
-     */
-    @Override
-    public AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] appTargets,
-            RemoteAnimationTargetCompat[] wallpaperTargets) {
-        if (mAnimationReadyListener != null) {
-            mAnimationReadyListener.onWindowAnimationCreated();
-        }
-        AnimatorSet anim = new AnimatorSet();
-        if (mRecentsView == null) {
-            if (Log.isLoggable(TAG, Log.WARN)) {
-                Log.w(TAG, "No recents view. Using stub animation.");
-            }
-            anim.play(ValueAnimator.ofInt(0, 1).setDuration(getRecentsLaunchDuration()));
-            return anim;
-        }
-
-        RemoteAnimationTargets targets =
-                new RemoteAnimationTargets(appTargets, wallpaperTargets, MODE_CLOSING);
-        mRecentsView.setTransitionedFromApp(!targets.isAnimatingHome());
-
-        RemoteAnimationTargetCompat recentsTarget = null;
-        RemoteAnimationTargetCompat closingAppTarget = null;
-
-        for (RemoteAnimationTargetCompat target : appTargets) {
-            if (target.mode == MODE_OPENING) {
-                recentsTarget = target;
-            } else if (target.mode == MODE_CLOSING && target.taskId == mTargetTaskId) {
-                closingAppTarget = target;
-            }
-        }
-
-        if (closingAppTarget == null) {
-            if (Log.isLoggable(TAG, Log.WARN)) {
-                Log.w(TAG, "No closing app target. Using stub animation.");
-            }
-            anim.play(ValueAnimator.ofInt(0, 1).setDuration(getRecentsLaunchDuration()));
-            return anim;
-        }
-        if (recentsTarget == null) {
-            if (Log.isLoggable(TAG, Log.WARN)) {
-                Log.w(TAG, "No recents target. Using stub animation.");
-            }
-            anim.play(ValueAnimator.ofInt(0, 1).setDuration(getRecentsLaunchDuration()));
-            return anim;
-        }
-
-        if (closingAppTarget.activityType == ACTIVITY_TYPE_HOME) {
-            mRecentsView.playRemoteHomeToRecentsAnimation(anim, closingAppTarget, recentsTarget);
-        } else {
-            mRecentsView.playRemoteAppToRecentsAnimation(anim, closingAppTarget, recentsTarget);
-        }
-
-        return anim;
-    }
-
-    @Override
-    public ActivityOptions toActivityOptions(Handler handler, long duration, Context context) {
-        LauncherAnimationRunner runner = new LauncherAnimationRunner(handler,
-                false /* startAtFrontOfQueue */) {
-
-            @Override
-            public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
-                    RemoteAnimationTargetCompat[] wallpaperTargets,
-                    AnimationResult result) {
-                IconRecentsView recentsView = mRecentsView;
-                if (!recentsView.isReadyForRemoteAnim()) {
-                    recentsView.setOnReadyForRemoteAnimCallback(() -> postAsyncCallback(handler,
-                            () -> onCreateAnimation(appTargets, wallpaperTargets, result))
-                    );
-                    return;
-                }
-                result.setAnimation(createWindowAnimation(appTargets, wallpaperTargets), context);
-            }
-        };
-        return ActivityOptionsCompat.makeRemoteAnimation(
-                new RemoteAnimationAdapterCompat(runner, duration,
-                        0 /* statusBarTransitionDelay */));
-    }
-
-    /**
-     * Get duration of animation from app to overview.
-     *
-     * @return duration of animation
-     */
-    long getRecentsLaunchDuration() {
-        return REMOTE_APP_TO_OVERVIEW_DURATION;
-    }
-
-    /**
-     * Listener for various points in the app to overview animation preparing to animate.
-     */
-    interface AppToOverviewAnimationListener {
-        /**
-         * Logic for when activity we're animating to is ready
-         *
-         * @param activity activity to animate to
-         */
-        void onActivityReady(BaseDraggingActivity activity);
-
-        /**
-         * Logic for when we've created the app to recents animation.
-         */
-        void onWindowAnimationCreated();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/ClearAllHolder.java b/go/quickstep/src/com/android/quickstep/ClearAllHolder.java
deleted file mode 100644
index ce87171..0000000
--- a/go/quickstep/src/com/android/quickstep/ClearAllHolder.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-
-/**
- * Holder for clear all button view in task recycler view.
- */
-final class ClearAllHolder extends ViewHolder {
-    public ClearAllHolder(@NonNull View itemView) {
-        super(itemView);
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java b/go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java
deleted file mode 100644
index 808cd72..0000000
--- a/go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java
+++ /dev/null
@@ -1,281 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static android.view.View.ALPHA;
-
-import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
-import static com.android.quickstep.views.TaskItemView.CONTENT_TRANSITION_PROGRESS;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-import androidx.recyclerview.widget.SimpleItemAnimator;
-
-import com.android.quickstep.views.TaskItemView;
-
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.List;
-
-/**
- * An item animator that is only set and used for the transition from the empty loading UI to
- * the filled task content UI. The animation starts from the bottom to top, changing all valid
- * empty item views to be filled and removing all extra empty views.
- */
-public final class ContentFillItemAnimator extends SimpleItemAnimator {
-
-    private static final class PendingAnimation {
-        ViewHolder viewHolder;
-        int animType;
-
-        PendingAnimation(ViewHolder vh, int type) {
-            viewHolder = vh;
-            animType = type;
-        }
-    }
-
-    private static final int ANIM_TYPE_REMOVE = 0;
-    private static final int ANIM_TYPE_CHANGE = 1;
-
-    private static final int ITEM_BETWEEN_DELAY = 40;
-    private static final int ITEM_CHANGE_DURATION = 150;
-    private static final int ITEM_REMOVE_DURATION = 150;
-
-    /**
-     * Animations that have been registered to occur together at the next call of
-     * {@link #runPendingAnimations()} but have not started.
-     */
-    private final ArrayList<PendingAnimation> mPendingAnims = new ArrayList<>();
-
-    /**
-     * Animations that have started and are running.
-     */
-    private final ArrayList<ObjectAnimator> mRunningAnims = new ArrayList<>();
-
-    private Runnable mOnFinishRunnable;
-
-    /**
-     * Set runnable to run after the content fill animation is fully completed.
-     *
-     * @param runnable runnable to run on end
-     */
-    public void setOnAnimationFinishedRunnable(Runnable runnable) {
-        mOnFinishRunnable = runnable;
-    }
-
-    @Override
-    public void setChangeDuration(long changeDuration) {
-        throw new UnsupportedOperationException("Cascading item animator cannot have animation "
-                + "duration changed.");
-    }
-
-    @Override
-    public void setRemoveDuration(long removeDuration) {
-        throw new UnsupportedOperationException("Cascading item animator cannot have animation "
-                + "duration changed.");
-    }
-
-    @Override
-    public boolean animateRemove(ViewHolder holder) {
-        PendingAnimation pendAnim = new PendingAnimation(holder, ANIM_TYPE_REMOVE);
-        mPendingAnims.add(pendAnim);
-        return true;
-    }
-
-    private void animateRemoveImpl(ViewHolder holder, long startDelay) {
-        final View view = holder.itemView;
-        if (holder.itemView.getAlpha() == 0) {
-            // View is already visually removed. We can just get rid of it now.
-            view.setAlpha(1.0f);
-            dispatchRemoveFinished(holder);
-            dispatchFinishedWhenDone();
-            return;
-        }
-        final ObjectAnimator anim = ObjectAnimator.ofFloat(
-                holder.itemView, ALPHA, holder.itemView.getAlpha(), 0.0f);
-        anim.setDuration(ITEM_REMOVE_DURATION).setStartDelay(startDelay);
-        anim.addListener(
-                new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationStart(Animator animation) {
-                        dispatchRemoveStarting(holder);
-                    }
-
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        view.setAlpha(1);
-                        dispatchRemoveFinished(holder);
-                        mRunningAnims.remove(anim);
-                        dispatchFinishedWhenDone();
-                    }
-                }
-        );
-        anim.start();
-        mRunningAnims.add(anim);
-    }
-
-    @Override
-    public boolean animateAdd(ViewHolder holder) {
-        dispatchAddFinished(holder);
-        return false;
-    }
-
-    @Override
-    public boolean animateMove(ViewHolder holder, int fromX, int fromY, int toX,
-            int toY) {
-        dispatchMoveFinished(holder);
-        return false;
-    }
-
-    @Override
-    public boolean animateChange(ViewHolder oldHolder,
-            ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
-        // Only support changes where the holders are the same
-        if (oldHolder == newHolder) {
-            PendingAnimation pendAnim = new PendingAnimation(oldHolder, ANIM_TYPE_CHANGE);
-            mPendingAnims.add(pendAnim);
-            return true;
-        }
-        dispatchChangeFinished(oldHolder, true /* oldItem */);
-        dispatchChangeFinished(newHolder, false /* oldItem */);
-        return false;
-    }
-
-    private void animateChangeImpl(ViewHolder viewHolder, long startDelay) {
-        TaskItemView itemView = (TaskItemView) viewHolder.itemView;
-        if (itemView.getAlpha() == 0) {
-            // View is still not visible, so we can finish the change immediately.
-            CONTENT_TRANSITION_PROGRESS.set(itemView, 1.0f);
-            dispatchChangeFinished(viewHolder, true /* oldItem */);
-            dispatchFinishedWhenDone();
-            return;
-        }
-        final ObjectAnimator anim =
-                ObjectAnimator.ofFloat(itemView, CONTENT_TRANSITION_PROGRESS, 0.0f, 1.0f);
-        anim.setDuration(ITEM_CHANGE_DURATION).setStartDelay(startDelay);
-        anim.addListener(
-                new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationStart(Animator animation) {
-                        dispatchChangeStarting(viewHolder, true /* oldItem */);
-                    }
-
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        CONTENT_TRANSITION_PROGRESS.set(itemView, 1.0f);
-                        dispatchChangeFinished(viewHolder, true /* oldItem */);
-                        mRunningAnims.remove(anim);
-                        dispatchFinishedWhenDone();
-                    }
-                }
-        );
-        anim.start();
-        mRunningAnims.add(anim);
-    }
-
-    @Override
-    public void runPendingAnimations() {
-        // Run animations bottom to top.
-        mPendingAnims.sort(Comparator.comparingInt(o -> -o.viewHolder.itemView.getBottom()));
-        int delay = 0;
-        while (!mPendingAnims.isEmpty()) {
-            PendingAnimation curAnim = mPendingAnims.remove(0);
-            ViewHolder vh = curAnim.viewHolder;
-            switch (curAnim.animType) {
-                case ANIM_TYPE_REMOVE:
-                    animateRemoveImpl(vh, delay);
-                    break;
-                case ANIM_TYPE_CHANGE:
-                    animateChangeImpl(vh, delay);
-                    break;
-                default:
-                    break;
-            }
-            delay += ITEM_BETWEEN_DELAY;
-        }
-    }
-
-    @Override
-    public void endAnimation(@NonNull ViewHolder item) {
-        for (int i = mPendingAnims.size() - 1; i >= 0; i--) {
-            endPendingAnimation(mPendingAnims.get(i));
-            mPendingAnims.remove(i);
-        }
-        dispatchFinishedWhenDone();
-    }
-
-    @Override
-    public void endAnimations() {
-        if (!isRunning()) {
-            return;
-        }
-        for (int i = mPendingAnims.size() - 1; i >= 0; i--) {
-            endPendingAnimation(mPendingAnims.get(i));
-            mPendingAnims.remove(i);
-        }
-        for (int i = mRunningAnims.size() - 1; i >= 0; i--) {
-            ObjectAnimator anim = mRunningAnims.get(i);
-            // This calls the on end animation callback which will set values to their end target.
-            anim.cancel();
-        }
-        dispatchFinishedWhenDone();
-    }
-
-    private void endPendingAnimation(PendingAnimation pendAnim) {
-        ViewHolder item = pendAnim.viewHolder;
-        switch (pendAnim.animType) {
-            case ANIM_TYPE_REMOVE:
-                item.itemView.setAlpha(1.0f);
-                dispatchRemoveFinished(item);
-                break;
-            case ANIM_TYPE_CHANGE:
-                CONTENT_TRANSITION_PROGRESS.set(item.itemView, 1.0f);
-                dispatchChangeFinished(item, true /* oldItem */);
-                break;
-            default:
-                break;
-        }
-    }
-
-    @Override
-    public boolean isRunning() {
-        return !mPendingAnims.isEmpty() || !mRunningAnims.isEmpty();
-    }
-
-    @Override
-    public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
-            @NonNull List<Object> payloads) {
-        if (!payloads.isEmpty()
-                && (int) payloads.get(0) == CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT) {
-            return true;
-        }
-        return super.canReuseUpdatedViewHolder(viewHolder, payloads);
-    }
-
-    private void dispatchFinishedWhenDone() {
-        if (!isRunning()) {
-            dispatchAnimationsFinished();
-            if (mOnFinishRunnable != null) {
-                mOnFinishRunnable.run();
-            }
-        }
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/FallbackActivityInterface.java b/go/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
deleted file mode 100644
index ecb9472..0000000
--- a/go/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.views.IconRecentsView.CONTENT_ALPHA;
-
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.graphics.Rect;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.util.ActivityInitListener;
-import com.android.quickstep.views.IconRecentsView;
-
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-
-/**
- * {@link BaseActivityInterface} for recents when the default launcher is different than the
- * currently running one and apps should interact with the {@link RecentsActivity} as opposed
- * to the in-launcher one.
- */
-public final class FallbackActivityInterface extends
-        GoActivityInterface<RecentsActivity> {
-
-    public FallbackActivityInterface() { }
-
-    @Override
-    public AnimationFactory prepareRecentsUI(boolean activityVisible,
-            boolean animateActivity, Consumer<AnimatorPlaybackController> callback) {
-        if (activityVisible) {
-            return (transitionLength) -> { };
-        }
-
-        RecentsActivity activity = getCreatedActivity();
-        IconRecentsView rv = activity.getOverviewPanel();
-        rv.setUsingRemoteAnimation(true);
-        rv.setAlpha(0);
-
-        return new AnimationFactory() {
-
-            boolean isAnimatingToRecents = false;
-
-            @Override
-            public void onRemoteAnimationReceived(RemoteAnimationTargets targets) {
-                isAnimatingToRecents = targets != null && targets.isAnimatingHome();
-                if (!isAnimatingToRecents) {
-                    rv.setAlpha(1);
-                }
-                createActivityInterface(getSwipeUpDestinationAndLength(
-                        activity.getDeviceProfile(), activity, new Rect()));
-            }
-
-            @Override
-            public void createActivityInterface(long transitionLength) {
-                if (!isAnimatingToRecents) {
-                    return;
-                }
-
-                ObjectAnimator anim = ObjectAnimator.ofFloat(rv, CONTENT_ALPHA, 0, 1);
-                anim.setDuration(transitionLength).setInterpolator(LINEAR);
-                AnimatorSet animatorSet = new AnimatorSet();
-                animatorSet.play(anim);
-                callback.accept(AnimatorPlaybackController.wrap(animatorSet, transitionLength));
-            }
-        };
-    }
-
-    @Override
-    public ActivityInitListener createActivityInitListener(
-            Predicate<Boolean> onInitListener) {
-        return new ActivityInitListener<>((activity, alreadyOnHome) ->
-                onInitListener.test(alreadyOnHome), RecentsActivity.ACTIVITY_TRACKER);
-    }
-
-    @Nullable
-    @Override
-    public RecentsActivity getCreatedActivity() {
-        return RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity();
-    }
-
-    @Nullable
-    @Override
-    public IconRecentsView getVisibleRecentsView() {
-        RecentsActivity activity = getCreatedActivity();
-        if (activity != null && activity.hasWindowFocus()) {
-            return activity.getOverviewPanel();
-        }
-        return null;
-    }
-
-    @Override
-    public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
-        return false;
-    }
-
-    @Override
-    public int getContainerType() {
-        return LauncherLogProto.ContainerType.SIDELOADED_LAUNCHER;
-    }
-
-    @Override
-    public void onLaunchTaskSuccess() { }
-}
diff --git a/go/quickstep/src/com/android/quickstep/FallbackRecentsToActivityHelper.java b/go/quickstep/src/com/android/quickstep/FallbackRecentsToActivityHelper.java
deleted file mode 100644
index a845f93..0000000
--- a/go/quickstep/src/com/android/quickstep/FallbackRecentsToActivityHelper.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-/**
- * {@link RecentsToActivityHelper} for when we are using the fallback recents in
- * {@link BaseRecentsActivity}.
- */
-public final class FallbackRecentsToActivityHelper implements RecentsToActivityHelper {
-
-    BaseRecentsActivity mActivity;
-
-    public FallbackRecentsToActivityHelper(BaseRecentsActivity activity) {
-        mActivity = activity;
-    }
-
-    @Override
-    public void leaveRecents() {
-        mActivity.startHome();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/GoActivityInterface.java b/go/quickstep/src/com/android/quickstep/GoActivityInterface.java
deleted file mode 100644
index b62d17c..0000000
--- a/go/quickstep/src/com/android/quickstep/GoActivityInterface.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.android.quickstep;
-
-import android.content.Context;
-import android.graphics.Rect;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.DeviceProfile;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-
-/**
- * Base activity control helper for Go that stubs out most of the functionality that is not needed
- * for Go.
- *
- * @param <T> activity that contains the overview
- */
-public abstract class GoActivityInterface<T extends BaseDraggingActivity> implements
-        BaseActivityInterface<T> {
-
-    @Override
-    public void onTransitionCancelled(boolean activityVisible) {
-        // Go transitions to overview are all atomic.
-    }
-
-    @Override
-    public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
-        // TODO Implement outRect depending on where the task should animate to.
-        // Go does not support swipe up gesture.
-        return 0;
-    }
-
-    @Override
-    public void onSwipeUpToRecentsComplete() {
-        // Go does not support swipe up gesture.
-    }
-
-    @Override
-    public void onAssistantVisibilityChanged(float visibility) {
-        // Go does not support assistant visibility transitions.
-    }
-
-    @Override
-    public HomeAnimationFactory prepareHomeUI() {
-        // Go does not support gestures from app to home.
-        return null;
-    }
-
-    @Override
-    public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) {
-        // Go does not support gestures to overview.
-        return null;
-    }
-
-    @Override
-    public boolean shouldMinimizeSplitScreen() {
-        // Go does not support split screen.
-        return true;
-    }
-
-    @Override
-    public boolean isInLiveTileMode() {
-        // Go does not support live tiles.
-        return false;
-    }
-
-    @Override
-    public void onLaunchTaskFailed() {
-        // Go does not support gestures from one task to another.
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/IconRecentsFragment.java b/go/quickstep/src/com/android/quickstep/IconRecentsFragment.java
deleted file mode 100644
index facf0d2..0000000
--- a/go/quickstep/src/com/android/quickstep/IconRecentsFragment.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep;
-
-import android.app.Fragment;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.R;
-
-public class IconRecentsFragment extends Fragment {
-    @Nullable
-    @Override
-    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
-            @Nullable Bundle savedInstanceState) {
-        return inflater.inflate(R.layout.icon_recents_root_view, container, false);
-    }
-}
\ No newline at end of file
diff --git a/go/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/go/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
deleted file mode 100644
index 3e93480..0000000
--- a/go/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep;
-
-import static com.android.launcher3.LauncherState.OVERVIEW;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherInitListener;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.anim.AnimatorPlaybackController;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.views.IconRecentsView;
-
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-
-/**
- * {@link BaseActivityInterface} for the in-launcher recents.
- * TODO: Implement the app to overview animation functionality
- */
-public final class LauncherActivityInterface extends GoActivityInterface<Launcher> {
-
-    @Override
-    public AnimationFactory prepareRecentsUI(boolean activityVisible, boolean animateActivity,
-            Consumer<AnimatorPlaybackController> callback) {
-        Launcher launcher = getCreatedActivity();
-        LauncherState fromState = launcher.getStateManager().getState();
-        launcher.<IconRecentsView>getOverviewPanel().setUsingRemoteAnimation(true);
-        //TODO: Implement this based off where the recents view needs to be for app => recents anim.
-        return new AnimationFactory() {
-            public void createActivityInterface(long transitionLength) {
-                callback.accept(launcher.getStateManager().createAnimationToNewWorkspace(
-                        fromState, OVERVIEW, transitionLength));
-            }
-
-            @Override
-            public void onTransitionCancelled() {}
-        };
-    }
-
-    @Override
-    public LauncherInitListener createActivityInitListener(Predicate<Boolean> onInitListener) {
-        return new LauncherInitListener((activity, alreadyOnHome) ->
-                onInitListener.test(alreadyOnHome));
-    }
-
-    @Override
-    public Launcher getCreatedActivity() {
-        LauncherAppState app = LauncherAppState.getInstanceNoCreate();
-        if (app == null) {
-            return null;
-        }
-        return (Launcher) app.getModel().getCallback();
-    }
-
-    private Launcher getVisibleLauncher() {
-        Launcher launcher = getCreatedActivity();
-        return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus() ?
-                launcher : null;
-    }
-
-    @Override
-    public IconRecentsView getVisibleRecentsView() {
-        Launcher launcher = getVisibleLauncher();
-        return launcher != null && launcher.getStateManager().getState().overviewUi
-                ? launcher.getOverviewPanel() : null;
-    }
-
-    @Override
-    public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
-        Launcher launcher = getVisibleLauncher();
-        if (launcher == null) {
-            return false;
-        }
-        launcher.<IconRecentsView>getOverviewPanel().setUsingRemoteAnimation(false);
-        launcher.getUserEventDispatcher().logActionCommand(
-                LauncherLogProto.Action.Command.RECENTS_BUTTON,
-                getContainerType(),
-                LauncherLogProto.ContainerType.TASKSWITCHER);
-        launcher.getStateManager().goToState(OVERVIEW,
-                launcher.getStateManager().shouldAnimateStateChange(), onCompleteCallback);
-        return true;
-    }
-
-    @Override
-    public int getContainerType() {
-        final Launcher launcher = getVisibleLauncher();
-        return launcher != null ? launcher.getStateManager().getState().containerType
-                : LauncherLogProto.ContainerType.APP;
-    }
-
-    @Override
-    public void onLaunchTaskSuccess() {
-        Launcher launcher = getCreatedActivity();
-        launcher.getStateManager().moveToRestState();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/go/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
deleted file mode 100644
index a436ce7..0000000
--- a/go/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.os.SystemClock;
-import android.view.ViewConfiguration;
-
-import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.quickstep.AppToOverviewAnimationProvider.AppToOverviewAnimationListener;
-import com.android.quickstep.util.ActivityInitListener;
-import com.android.quickstep.views.IconRecentsView;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.LatencyTrackerCompat;
-
-/**
- * Helper class to handle various atomic commands for switching between Overview.
- */
-@TargetApi(Build.VERSION_CODES.P)
-public class OverviewCommandHelper {
-
-    private final Context mContext;
-    private final RecentsAnimationDeviceState mDeviceState;
-    private final RecentsModel mRecentsModel;
-    private final OverviewComponentObserver mOverviewComponentObserver;
-
-    private long mLastToggleTime;
-
-    public OverviewCommandHelper(Context context, RecentsAnimationDeviceState deviceState,
-            OverviewComponentObserver observer) {
-        mContext = context;
-        mDeviceState = deviceState;
-        mRecentsModel = RecentsModel.INSTANCE.get(mContext);
-        mOverviewComponentObserver = observer;
-    }
-
-    public void onOverviewToggle() {
-        // If currently screen pinning, do not enter overview
-        if (mDeviceState.isScreenPinningActive()) {
-            return;
-        }
-
-        ActivityManagerWrapper.getInstance()
-                .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
-        MAIN_EXECUTOR.execute(new RecentsActivityCommand<>());
-    }
-
-    public void onOverviewShown(boolean triggeredFromAltTab) {
-        MAIN_EXECUTOR.execute(new ShowRecentsCommand());
-    }
-
-    public void onOverviewHidden() {
-        MAIN_EXECUTOR.execute(new HideRecentsCommand());
-    }
-
-    public void onTip(int actionType, int viewType) {
-        MAIN_EXECUTOR.execute(() ->
-                UserEventDispatcher.newInstance(mContext).logActionTip(actionType, viewType));
-    }
-
-    private class ShowRecentsCommand extends RecentsActivityCommand {
-
-        @Override
-        protected boolean handleCommand(long elapsedTime) {
-            return mHelper.getVisibleRecentsView() != null;
-        }
-    }
-
-    private class HideRecentsCommand extends RecentsActivityCommand {
-
-        @Override
-        protected boolean handleCommand(long elapsedTime) {
-            IconRecentsView recents = (IconRecentsView) mHelper.getVisibleRecentsView();
-            if (recents == null) {
-                return false;
-            }
-            recents.handleOverviewCommand();
-            return true;
-        }
-    }
-
-    private class RecentsActivityCommand<T extends BaseDraggingActivity> implements Runnable {
-
-        protected final BaseActivityInterface<T> mHelper;
-        private final long mCreateTime;
-
-        private final long mToggleClickedTime = SystemClock.uptimeMillis();
-        private boolean mUserEventLogged;
-        private ActivityInitListener<T> mListener;
-
-        public RecentsActivityCommand() {
-            mHelper = mOverviewComponentObserver.getActivityInterface();
-            mCreateTime = SystemClock.elapsedRealtime();
-
-            // Preload the plan
-            mRecentsModel.getTasks(null);
-        }
-
-        @Override
-        public void run() {
-            long elapsedTime = mCreateTime - mLastToggleTime;
-            mLastToggleTime = mCreateTime;
-
-            if (handleCommand(elapsedTime)) {
-                // Command already handled.
-                return;
-            }
-
-            if (mHelper.switchToRecentsIfVisible(null /* onCompleteCallback */)) {
-                // If successfully switched, then return
-                return;
-            }
-
-            AppToOverviewAnimationProvider<T> provider =
-                    new AppToOverviewAnimationProvider<>(mHelper, RecentsModel.getRunningTaskId());
-            provider.setAnimationListener(
-                    new AppToOverviewAnimationListener() {
-                        @Override
-                        public void onActivityReady(BaseDraggingActivity activity) {
-                            if (!mUserEventLogged) {
-                                activity.getUserEventDispatcher().logActionCommand(
-                                        LauncherLogProto.Action.Command.RECENTS_BUTTON,
-                                        mHelper.getContainerType(),
-                                        LauncherLogProto.ContainerType.TASKSWITCHER);
-                                mUserEventLogged = true;
-                            }
-                        }
-
-                        @Override
-                        public void onWindowAnimationCreated() {
-                            if (LatencyTrackerCompat.isEnabled(mContext)) {
-                                LatencyTrackerCompat.logToggleRecents(
-                                        (int) (SystemClock.uptimeMillis() - mToggleClickedTime));
-                            }
-
-                            mListener.unregister();
-                        }
-                    });
-
-            // Otherwise, start overview.
-            mListener = mHelper.createActivityInitListener(provider::onActivityReady);
-            mListener.registerAndStartActivity(mOverviewComponentObserver.getOverviewIntent(),
-                    provider, mContext, MAIN_EXECUTOR.getHandler(),
-                    provider.getRecentsLaunchDuration());
-        }
-
-        protected boolean handleCommand(long elapsedTime) {
-            IconRecentsView recents = mHelper.getVisibleRecentsView();
-            if (recents != null) {
-                recents.handleOverviewCommand();
-                return true;
-            } else if (elapsedTime < ViewConfiguration.getDoubleTapTimeout()) {
-                // The user tried to launch back into overview too quickly, either after
-                // launching an app, or before overview has actually shown, just ignore for now
-                return true;
-            }
-            return false;
-        }
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/RecentsActivity.java b/go/quickstep/src/com/android/quickstep/RecentsActivity.java
deleted file mode 100644
index 7f813ce..0000000
--- a/go/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import android.app.ActivityOptions;
-import android.view.View;
-
-import com.android.launcher3.R;
-import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.fallback.GoRecentsActivityRootView;
-import com.android.quickstep.views.IconRecentsView;
-
-/**
- * A recents activity that displays recent tasks with an icon and small snapshot.
- */
-public final class RecentsActivity extends BaseRecentsActivity {
-
-    private GoRecentsActivityRootView mRecentsRootView;
-    private IconRecentsView mIconRecentsView;
-
-    @Override
-    protected void initViews() {
-        setContentView(R.layout.fallback_recents_activity);
-        mRecentsRootView = findViewById(R.id.drag_layer);
-        mIconRecentsView = findViewById(R.id.overview_panel);
-        mIconRecentsView.setRecentsToActivityHelper(new FallbackRecentsToActivityHelper(this));
-        mIconRecentsView.setShowStatusBarForegroundScrim(true);
-    }
-
-    @Override
-    protected void reapplyUi() {
-        // No-op. Insets are automatically re-applied in the root view.
-    }
-
-    @Override
-    public BaseDragLayer getDragLayer() {
-        return mRecentsRootView;
-    }
-
-    @Override
-    public View getRootView() {
-        return mRecentsRootView;
-    }
-
-    @Override
-    public <T extends View> T getOverviewPanel() {
-        return (T) mIconRecentsView;
-    }
-
-    @Override
-    public ActivityOptions getActivityLaunchOptions(View v) {
-        // Stubbed. Recents launch animation will come from the recents view itself.
-        return null;
-    }
-
-    @Override
-    protected void onResume() {
-        mIconRecentsView.onBeginTransitionToOverview();
-        super.onResume();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/RecentsToActivityHelper.java b/go/quickstep/src/com/android/quickstep/RecentsToActivityHelper.java
deleted file mode 100644
index 8f3b707..0000000
--- a/go/quickstep/src/com/android/quickstep/RecentsToActivityHelper.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-/**
- * Generic interface providing methods to the recents implementation that allow it to callback to
- * the containing activity.
- */
-public interface RecentsToActivityHelper {
-
-    /**
-     * The default action to take when leaving/closing recents. In general, this should be used to
-     * go to the appropriate home state.
-     */
-    void leaveRecents();
-}
diff --git a/go/quickstep/src/com/android/quickstep/TaskActionController.java b/go/quickstep/src/com/android/quickstep/TaskActionController.java
deleted file mode 100644
index f49fa3e..0000000
--- a/go/quickstep/src/com/android/quickstep/TaskActionController.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
-import static com.android.quickstep.TaskUtils.getLaunchComponentKeyForTask;
-
-import android.app.ActivityOptions;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.logging.StatsLogManager;
-import com.android.quickstep.views.TaskItemView;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.Task.TaskKey;
-import com.android.systemui.shared.system.ActivityManagerWrapper;
-
-/**
- * Controller that provides logic for task-related commands on recents and updating the model/view
- * as appropriate.
- */
-public final class TaskActionController {
-
-    private final TaskListLoader mLoader;
-    private final TaskAdapter mAdapter;
-    private final StatsLogManager mStatsLogManager;
-
-    public TaskActionController(TaskListLoader loader, TaskAdapter adapter,
-            StatsLogManager logManager) {
-        mLoader = loader;
-        mAdapter = adapter;
-        mStatsLogManager = logManager;
-    }
-
-    /**
-     * Launch the task associated with the task holder, animating into the app from the task view.
-     *
-     * @param viewHolder the task view holder to launch
-     */
-    public void launchTaskFromView(@NonNull TaskHolder viewHolder) {
-        if (!viewHolder.getTask().isPresent()) {
-            return;
-        }
-        TaskItemView itemView = (TaskItemView) (viewHolder.itemView);
-        View v = itemView.getThumbnailView();
-        int left = 0;
-        int top = 0;
-        int width = v.getMeasuredWidth();
-        int height = v.getMeasuredHeight();
-
-        TaskKey key = viewHolder.getTask().get().key;
-        ActivityOptions opts = ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
-        ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(key, opts,
-                null /* resultCallback */, null /* resultCallbackHandler */);
-        mStatsLogManager.logTaskLaunch(null /* view */, getLaunchComponentKeyForTask(key));
-    }
-
-    /**
-     * Launch the task directly with a basic animation.
-     *
-     * @param task the task to launch
-     */
-    public void launchTask(@NonNull Task task) {
-        ActivityOptions opts = ActivityOptions.makeBasic();
-        ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(task.key, opts,
-                null /* resultCallback */, null /* resultCallbackHandler */);
-        mStatsLogManager.logTaskLaunch(null /* view */, getLaunchComponentKeyForTask(task.key));
-    }
-
-    /**
-     * Removes the task holder and the task, updating the model and the view.
-     *
-     * @param viewHolder the task view holder to remove
-     */
-    public void removeTask(TaskHolder viewHolder) {
-        if (!viewHolder.getTask().isPresent()) {
-            return;
-        }
-        int position = viewHolder.getAdapterPosition();
-        Task task = viewHolder.getTask().get();
-        ActivityManagerWrapper.getInstance().removeTask(task.key.id);
-        mLoader.removeTask(task);
-        mAdapter.notifyItemRemoved(position);
-        // TODO(b/131840601): Add logging point for removal.
-    }
-
-    /**
-     * Clears all tasks and updates the model and view.
-     */
-    public void clearAllTasks() {
-        int count = mAdapter.getItemCount();
-        ActivityManagerWrapper.getInstance().removeAllRecentTasks();
-        mLoader.clearAllTasks();
-        mAdapter.notifyItemRangeRemoved(TASKS_START_POSITION /* positionStart */, count);
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/TaskAdapter.java b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
deleted file mode 100644
index 509bf29..0000000
--- a/go/quickstep/src/com/android/quickstep/TaskAdapter.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.ViewGroup;
-import android.widget.Button;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView.Adapter;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-
-import com.android.launcher3.R;
-import com.android.quickstep.views.TaskItemView;
-import com.android.systemui.shared.recents.model.Task;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Recycler view adapter that dynamically inflates and binds {@link TaskHolder} instances with the
- * appropriate {@link Task} from the recents task list.
- */
-public final class TaskAdapter extends Adapter<ViewHolder> {
-
-    public static final int CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT = 0;
-    public static final int MAX_TASKS_TO_DISPLAY = 6;
-    public static final int TASKS_START_POSITION = 1;
-
-    public static final int ITEM_TYPE_TASK = 0;
-    public static final int ITEM_TYPE_CLEAR_ALL = 1;
-
-    private static final String TAG = "TaskAdapter";
-    private final TaskListLoader mLoader;
-    private TaskActionController mTaskActionController;
-    private OnClickListener mClearAllListener;
-    private boolean mIsShowingLoadingUi;
-
-    public TaskAdapter(@NonNull TaskListLoader loader) {
-        mLoader = loader;
-    }
-
-    public void setActionController(TaskActionController taskActionController) {
-        mTaskActionController = taskActionController;
-    }
-
-    public void setOnClearAllClickListener(OnClickListener listener) {
-        mClearAllListener = listener;
-    }
-
-    /**
-     * Sets all positions in the task adapter to loading views, binding new views if necessary.
-     * This changes the task adapter's view of the data, so the appropriate notify events should be
-     * called in addition to this method to reflect the changes.
-     *
-     * @param isShowingLoadingUi true to bind loading task views to all positions, false to return
-     *                           to the real data
-     */
-    public void setIsShowingLoadingUi(boolean isShowingLoadingUi) {
-        mIsShowingLoadingUi = isShowingLoadingUi;
-    }
-
-    @Override
-    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-        switch (viewType) {
-            case ITEM_TYPE_TASK:
-                TaskItemView itemView = (TaskItemView) LayoutInflater.from(parent.getContext())
-                        .inflate(R.layout.task_item_view, parent, false);
-                TaskHolder taskHolder = new TaskHolder(itemView);
-                itemView.setOnClickListener(
-                        view -> mTaskActionController.launchTaskFromView(taskHolder));
-                return taskHolder;
-            case ITEM_TYPE_CLEAR_ALL:
-                View clearView = LayoutInflater.from(parent.getContext())
-                        .inflate(R.layout.clear_all_button, parent, false);
-                ClearAllHolder clearAllHolder = new ClearAllHolder(clearView);
-                Button clearViewButton = clearView.findViewById(R.id.clear_all_button);
-                clearViewButton.setOnClickListener(mClearAllListener);
-                return clearAllHolder;
-            default:
-                throw new IllegalArgumentException("No known holder for item type: " + viewType);
-        }
-    }
-
-    @Override
-    public void onBindViewHolder(ViewHolder holder, int position) {
-        onBindViewHolderInternal(holder, position, false /* willAnimate */);
-    }
-
-    @Override
-    public void onBindViewHolder(@NonNull ViewHolder holder, int position,
-            @NonNull List<Object> payloads) {
-        if (payloads.isEmpty()) {
-            super.onBindViewHolder(holder, position, payloads);
-            return;
-        }
-        int changeType = (int) payloads.get(0);
-        if (changeType == CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT) {
-            // Bind in preparation for animation
-            onBindViewHolderInternal(holder, position, true /* willAnimate */);
-        } else {
-            throw new IllegalArgumentException("Payload content is not a valid change event type: "
-                    + changeType);
-        }
-    }
-
-    private void onBindViewHolderInternal(@NonNull ViewHolder holder, int position,
-            boolean willAnimate) {
-        int itemType = getItemViewType(position);
-        switch (itemType) {
-            case ITEM_TYPE_TASK:
-                TaskHolder taskHolder = (TaskHolder) holder;
-                if (mIsShowingLoadingUi) {
-                    taskHolder.bindEmptyUi();
-                    return;
-                }
-                List<Task> tasks = mLoader.getCurrentTaskList();
-                int taskPos = position - TASKS_START_POSITION;
-                if (taskPos >= tasks.size()) {
-                    // Task list has updated.
-                    return;
-                }
-                Task task = tasks.get(taskPos);
-                taskHolder.bindTask(task, willAnimate /* willAnimate */);
-                mLoader.loadTaskIconAndLabel(task, () -> {
-                    // Ensure holder still has the same task.
-                    if (Objects.equals(Optional.of(task), taskHolder.getTask())) {
-                        taskHolder.getTaskItemView().setIcon(task.icon);
-                        taskHolder.getTaskItemView().setLabel(task.titleDescription);
-                    }
-                });
-                mLoader.loadTaskThumbnail(task, () -> {
-                    if (Objects.equals(Optional.of(task), taskHolder.getTask())) {
-                        taskHolder.getTaskItemView().setThumbnail(task.thumbnail);
-                    }
-                });
-                break;
-            case ITEM_TYPE_CLEAR_ALL:
-                // Nothing to bind.
-                break;
-            default:
-                throw new IllegalArgumentException("No known holder for item type: " + itemType);
-        }
-    }
-
-    @Override
-    public int getItemViewType(int position) {
-        // Bottom is always clear all button.
-        return (position == 0) ? ITEM_TYPE_CLEAR_ALL : ITEM_TYPE_TASK;
-    }
-
-    @Override
-    public int getItemCount() {
-        int itemCount = TASKS_START_POSITION;
-        if (mIsShowingLoadingUi) {
-            // Show loading version of all items.
-            itemCount += MAX_TASKS_TO_DISPLAY;
-        } else {
-            itemCount += Math.min(mLoader.getCurrentTaskList().size(), MAX_TASKS_TO_DISPLAY);
-        }
-        return itemCount;
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/TaskHolder.java b/go/quickstep/src/com/android/quickstep/TaskHolder.java
deleted file mode 100644
index 49b6aaa..0000000
--- a/go/quickstep/src/com/android/quickstep/TaskHolder.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-
-import com.android.quickstep.views.TaskItemView;
-import com.android.systemui.shared.recents.model.Task;
-
-import java.util.Optional;
-
-/**
- * A recycler view holder that holds the task view and binds {@link Task} content (app title, icon,
- * etc.) to the view.
- */
-public final class TaskHolder extends ViewHolder {
-
-    private final TaskItemView mTaskItemView;
-    private Task mTask;
-
-    public TaskHolder(TaskItemView itemView) {
-        super(itemView);
-        mTaskItemView = itemView;
-    }
-
-    public TaskItemView getTaskItemView() {
-        return mTaskItemView;
-    }
-
-    /**
-     * Bind the task model to the holder. This will take the current task content in the task
-     * object (i.e. icon, thumbnail, label) and either apply the content immediately or simply bind
-     * the content to animate to at a later time. If the task does not have all its content loaded,
-     * the view will prepare appropriate default placeholders and it is the callers responsibility
-     * to change them at a later time.
-     *
-     * Regardless of whether it is animating, input handlers will be bound immediately (see
-     * {@link TaskActionController}).
-     *
-     * @param task the task to bind to the view
-     * @param willAnimate true if UI should animate in later, false if it should apply immediately
-     */
-    public void bindTask(@NonNull Task task, boolean willAnimate) {
-        mTask = task;
-        if (willAnimate) {
-            mTaskItemView.startContentAnimation(task.icon, task.thumbnail, task.titleDescription);
-        } else {
-            mTaskItemView.setIcon(task.icon);
-            mTaskItemView.setThumbnail(task.thumbnail);
-            mTaskItemView.setLabel(task.titleDescription);
-        }
-    }
-
-    /**
-     * Bind a generic empty UI to the holder to make it clear that the item is loading/unbound and
-     * should not be expected to react to user input.
-     */
-    public void bindEmptyUi() {
-        mTask = null;
-        mTaskItemView.resetToEmptyUi();
-    }
-
-    /**
-     * Gets the task currently bound to this view. May be null if task holder is in a loading state.
-     *
-     * @return the current task
-     */
-    public Optional<Task> getTask() {
-        return Optional.ofNullable(mTask);
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/TaskListLoader.java b/go/quickstep/src/com/android/quickstep/TaskListLoader.java
deleted file mode 100644
index 1335cac..0000000
--- a/go/quickstep/src/com/android/quickstep/TaskListLoader.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep;
-
-import android.content.Context;
-
-import androidx.annotation.Nullable;
-
-import com.android.systemui.shared.recents.model.Task;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.function.Consumer;
-
-/**
- * This class is responsible for maintaining the list of tasks and the task content. The list must
- * be updated explicitly with {@link #loadTaskList} whenever the list needs to be
- * up-to-date.
- */
-public final class TaskListLoader {
-
-    private final RecentsModel mRecentsModel;
-
-    private ArrayList<Task> mTaskList = new ArrayList<>();
-    private int mTaskListChangeId;
-
-    public TaskListLoader(Context context) {
-        mRecentsModel = RecentsModel.INSTANCE.get(context);
-    }
-
-    /**
-     * Returns the current task list as of the last completed load (see {@link #loadTaskList}) as a
-     * read-only list. This list of tasks is not guaranteed to have all content loaded.
-     *
-     * @return the current list of tasks
-     */
-    public List<Task> getCurrentTaskList() {
-        return Collections.unmodifiableList(mTaskList);
-    }
-
-    /**
-     * Whether or not the loader needs to load data to be up to date. This can return true if the
-     * task list is already up to date OR there is already a load in progress for the task list to
-     * become up to date.
-     *
-     * @return true if already up to date or load in progress, false otherwise
-     */
-    public boolean needsToLoad() {
-        return !mRecentsModel.isTaskListValid(mTaskListChangeId);
-    }
-
-    /**
-     * Fetches the most recent tasks and updates the task list asynchronously. This call does not
-     * provide guarantees the task content (icon, thumbnail, label) are loaded but will fill in
-     * what it has. May run the callback immediately if there have been no changes in the task
-     * list since the start of the last load.
-     *
-     * @param onLoadedCallback callback to run when task list is loaded
-     */
-    public void loadTaskList(@Nullable Consumer<ArrayList<Task>> onLoadedCallback) {
-        if (!needsToLoad()) {
-            if (onLoadedCallback != null) {
-                onLoadedCallback.accept(mTaskList);
-            }
-            return;
-        }
-        // TODO: Look into error checking / more robust handling for when things go wrong.
-        mTaskListChangeId = mRecentsModel.getTasks(loadedTasks -> {
-            ArrayList<Task> tasks = new ArrayList<>(loadedTasks);
-            // Reverse tasks to put most recent at the bottom of the view
-            Collections.reverse(tasks);
-            // Load task content
-            for (Task task : tasks) {
-                int loadedPos = mTaskList.indexOf(task);
-                if (loadedPos == -1) {
-                    continue;
-                }
-                Task loadedTask = mTaskList.get(loadedPos);
-                task.icon = loadedTask.icon;
-                task.titleDescription = loadedTask.titleDescription;
-                task.thumbnail = loadedTask.thumbnail;
-            }
-            mTaskList = tasks;
-            onLoadedCallback.accept(tasks);
-        });
-    }
-
-    /**
-     * Load task icon and label asynchronously if it is not already loaded in the task. If the task
-     * already has an icon, this calls the callback immediately.
-     *
-     * @param task task to update with icon + label
-     * @param onLoadedCallback callback to run when task has icon and label
-     */
-    public void loadTaskIconAndLabel(Task task, @Nullable Runnable onLoadedCallback) {
-        mRecentsModel.getIconCache().updateIconInBackground(task,
-                loadedTask -> onLoadedCallback.run());
-    }
-
-    /**
-     * Load thumbnail asynchronously if not already loaded in the task. If the task already has a
-     * thumbnail or if the thumbnail is cached, this calls the callback immediately.
-     *
-     * @param task task to update with the thumbnail
-     * @param onLoadedCallback callback to run when task has thumbnail
-     */
-    public void loadTaskThumbnail(Task task, @Nullable Runnable onLoadedCallback) {
-        mRecentsModel.getThumbnailCache().updateThumbnailInBackground(task,
-                thumbnail -> onLoadedCallback.run());
-    }
-
-    /**
-     * Removes the task from the current task list.
-     */
-    void removeTask(Task task) {
-        mTaskList.remove(task);
-    }
-
-    /**
-     * Clears the current task list.
-     */
-    void clearAllTasks() {
-        mTaskList.clear();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java b/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java
deleted file mode 100644
index 57f49d6..0000000
--- a/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static androidx.recyclerview.widget.ItemTouchHelper.RIGHT;
-
-import static com.android.quickstep.TaskAdapter.ITEM_TYPE_CLEAR_ALL;
-
-import android.graphics.Canvas;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.ItemTouchHelper;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-
-import java.util.function.Consumer;
-
-/**
- * Callback for swipe input on {@link TaskHolder} views in the recents view.
- */
-public final class TaskSwipeCallback extends ItemTouchHelper.SimpleCallback {
-
-    private final Consumer<TaskHolder> mOnTaskSwipeCallback;
-
-    public TaskSwipeCallback(Consumer<TaskHolder> onTaskSwipeCallback) {
-        super(0 /* dragDirs */, RIGHT);
-        mOnTaskSwipeCallback = onTaskSwipeCallback;
-    }
-
-    @Override
-    public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder,
-            ViewHolder target) {
-        return false;
-    }
-
-    @Override
-    public void onSwiped(ViewHolder viewHolder, int direction) {
-        if (direction == RIGHT) {
-            mOnTaskSwipeCallback.accept((TaskHolder) viewHolder);
-        }
-    }
-
-    @Override
-    public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
-            @NonNull ViewHolder viewHolder, float dX, float dY, int actionState,
-            boolean isCurrentlyActive) {
-        if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
-            float alpha = 1.0f - dX / (float) viewHolder.itemView.getWidth();
-            viewHolder.itemView.setAlpha(alpha);
-        }
-        super.onChildDraw(c, recyclerView, viewHolder, dX, dY,
-                    actionState, isCurrentlyActive);
-    }
-
-    @Override
-    public int getSwipeDirs(@NonNull RecyclerView recyclerView,
-            @NonNull ViewHolder viewHolder) {
-        if (viewHolder.getItemViewType() == ITEM_TYPE_CLEAR_ALL) {
-            // Clear all button should not be swipable.
-            return 0;
-        }
-        return super.getSwipeDirs(recyclerView, viewHolder);
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/ThumbnailDrawable.java b/go/quickstep/src/com/android/quickstep/ThumbnailDrawable.java
deleted file mode 100644
index 922e68a..0000000
--- a/go/quickstep/src/com/android/quickstep/ThumbnailDrawable.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep;
-
-import static android.graphics.Shader.TileMode.CLAMP;
-
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapShader;
-import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.Drawable;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.R;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-
-/**
- * Bitmap backed drawable that supports rotating the thumbnail bitmap depending on if the
- * orientation the thumbnail was taken in matches the desired orientation. In addition, the
- * thumbnail always fills into the containing bounds.
- */
-public final class ThumbnailDrawable extends Drawable {
-
-    private final Paint mPaint = new Paint();
-    private final Matrix mMatrix = new Matrix();
-    private final ThumbnailData mThumbnailData;
-    private final BitmapShader mShader;
-    private final RectF mDestRect = new RectF();
-    private final int mCornerRadius;
-    private int mRequestedOrientation;
-
-    public ThumbnailDrawable(Resources res, @NonNull ThumbnailData thumbnailData,
-            int requestedOrientation) {
-        mThumbnailData = thumbnailData;
-        mRequestedOrientation = requestedOrientation;
-        mCornerRadius = (int) res.getDimension(R.dimen.task_thumbnail_corner_radius);
-        mShader = new BitmapShader(mThumbnailData.thumbnail, CLAMP, CLAMP);
-        mPaint.setShader(mShader);
-        mPaint.setAntiAlias(true);
-        updateMatrix();
-    }
-
-    /**
-     * Set the requested orientation.
-     *
-     * @param orientation the orientation we want the thumbnail to be in
-     */
-    public void setRequestedOrientation(int orientation) {
-        if (mRequestedOrientation != orientation) {
-            mRequestedOrientation = orientation;
-            updateMatrix();
-        }
-    }
-
-    @Override
-    public void draw(Canvas canvas) {
-        if (mThumbnailData.thumbnail == null) {
-            return;
-        }
-        canvas.drawRoundRect(mDestRect, mCornerRadius, mCornerRadius, mPaint);
-    }
-
-    @Override
-    protected void onBoundsChange(Rect bounds) {
-        super.onBoundsChange(bounds);
-        mDestRect.set(bounds);
-        updateMatrix();
-    }
-
-    @Override
-    public void setAlpha(int alpha) {
-        final int oldAlpha = mPaint.getAlpha();
-        if (alpha != oldAlpha) {
-            mPaint.setAlpha(alpha);
-            invalidateSelf();
-        }
-    }
-
-    @Override
-    public int getAlpha() {
-        return mPaint.getAlpha();
-    }
-
-    @Override
-    public void setColorFilter(ColorFilter colorFilter) {
-        mPaint.setColorFilter(colorFilter);
-        invalidateSelf();
-    }
-
-    @Override
-    public ColorFilter getColorFilter() {
-        return mPaint.getColorFilter();
-    }
-
-    @Override
-    public int getOpacity() {
-        return PixelFormat.TRANSLUCENT;
-    }
-
-    private void updateMatrix() {
-        if (mThumbnailData.thumbnail == null) {
-            return;
-        }
-        mMatrix.reset();
-        float scaleX;
-        float scaleY;
-        Rect bounds = getBounds();
-        Bitmap thumbnail = mThumbnailData.thumbnail;
-        if (mRequestedOrientation != mThumbnailData.orientation) {
-            // Rotate and translate so that top left is the same.
-            mMatrix.postRotate(90, 0, 0);
-            mMatrix.postTranslate(thumbnail.getHeight(), 0);
-
-            scaleX = (float) bounds.width() / thumbnail.getHeight();
-            scaleY = (float) bounds.height() / thumbnail.getWidth();
-        } else {
-            scaleX = (float) bounds.width() / thumbnail.getWidth();
-            scaleY = (float) bounds.height() / thumbnail.getHeight();
-        }
-        // Scale to fill.
-        mMatrix.postScale(scaleX, scaleY);
-        mShader.setLocalMatrix(mMatrix);
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/TouchInteractionService.java b/go/quickstep/src/com/android/quickstep/TouchInteractionService.java
deleted file mode 100644
index 6bb1c66..0000000
--- a/go/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
-
-import android.annotation.TargetApi;
-import android.app.Service;
-import android.content.Intent;
-import android.graphics.Region;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.util.Log;
-import android.view.MotionEvent;
-
-import com.android.systemui.shared.recents.IOverviewProxy;
-import com.android.systemui.shared.recents.ISystemUiProxy;
-
-/**
- * Service connected by system-UI for handling touch interaction.
- */
-@TargetApi(Build.VERSION_CODES.O)
-public class TouchInteractionService extends Service {
-
-    private static final String TAG = "GoTouchInteractionService";
-
-    private final IBinder mMyBinder = new IOverviewProxy.Stub() {
-
-        @Override
-        public void onActiveNavBarRegionChanges(Region region) throws RemoteException { }
-
-        @Override
-        public void onInitialize(Bundle bundle) throws RemoteException {
-            ISystemUiProxy iSystemUiProxy = ISystemUiProxy.Stub
-                    .asInterface(bundle.getBinder(KEY_EXTRA_SYSUI_PROXY));
-            SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(iSystemUiProxy);
-        }
-
-        @Override
-        public void onOverviewToggle() {
-            if (mDeviceState.isUserUnlocked()) {
-                mOverviewCommandHelper.onOverviewToggle();
-            }
-        }
-
-        @Override
-        public void onOverviewShown(boolean triggeredFromAltTab) {
-            if (mDeviceState.isUserUnlocked()) {
-                mOverviewCommandHelper.onOverviewShown(triggeredFromAltTab);
-            }
-        }
-
-        @Override
-        public void onOverviewHidden(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
-            if (mDeviceState.isUserUnlocked() && triggeredFromAltTab && !triggeredFromHomeKey) {
-                // onOverviewShownFromAltTab hides the overview and ends at the target app
-                mOverviewCommandHelper.onOverviewHidden();
-            }
-        }
-
-        @Override
-        public void onTip(int actionType, int viewType) {
-            if (mDeviceState.isUserUnlocked()) {
-                mOverviewCommandHelper.onTip(actionType, viewType);
-            }
-        }
-
-        @Override
-        public void onAssistantAvailable(boolean available) {
-            // TODO handle assistant
-        }
-
-        @Override
-        public void onAssistantVisibilityChanged(float visibility) {
-            // TODO handle assistant
-        }
-
-        public void onBackAction(boolean completed, int downX, int downY, boolean isButton,
-                boolean gestureSwipeLeft) {
-        }
-
-        public void onSystemUiStateChanged(int stateFlags) {
-            // To be implemented
-        }
-
-        /** Deprecated methods **/
-        public void onQuickStep(MotionEvent motionEvent) { }
-
-        public void onQuickScrubEnd() { }
-
-        public void onQuickScrubProgress(float progress) { }
-
-        public void onQuickScrubStart() { }
-
-        public void onPreMotionEvent(int downHitTarget) { }
-
-        public void onMotionEvent(MotionEvent ev) { }
-
-        public void onBind(ISystemUiProxy iSystemUiProxy) {
-            SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(iSystemUiProxy);
-        }
-    };
-
-    private static boolean sConnected = false;;
-
-    public static boolean isConnected() {
-        return sConnected;
-    }
-
-    private RecentsModel mRecentsModel;
-    private OverviewComponentObserver mOverviewComponentObserver;
-    private OverviewCommandHelper mOverviewCommandHelper;
-    private RecentsAnimationDeviceState mDeviceState;
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        mDeviceState = new RecentsAnimationDeviceState(this);
-        mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
-
-        sConnected = true;
-    }
-
-    public void onUserUnlocked() {
-        mRecentsModel = RecentsModel.INSTANCE.get(this);
-        mOverviewComponentObserver = new OverviewComponentObserver(this, mDeviceState);
-        mOverviewCommandHelper = new OverviewCommandHelper(this, mDeviceState,
-                mOverviewComponentObserver);
-    }
-
-    @Override
-    public void onDestroy() {
-        if (mDeviceState.isUserUnlocked()) {
-            mOverviewComponentObserver.onDestroy();
-        }
-        mDeviceState.destroy();
-        sConnected = false;
-        super.onDestroy();
-    }
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "Touch service connected");
-        }
-        return mMyBinder;
-    }
-
-    public static boolean isInitialized() {
-        return true;
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/fallback/GoRecentsActivityRootView.java b/go/quickstep/src/com/android/quickstep/fallback/GoRecentsActivityRootView.java
deleted file mode 100644
index b550011..0000000
--- a/go/quickstep/src/com/android/quickstep/fallback/GoRecentsActivityRootView.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.fallback;
-
-import android.content.Context;
-import android.graphics.Insets;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.WindowInsets;
-
-import com.android.launcher3.util.TouchController;
-import com.android.launcher3.views.BaseDragLayer;
-import com.android.quickstep.RecentsActivity;
-
-/**
- * Minimal implementation of {@link BaseDragLayer} for Go's fallback recents activity.
- */
-public final class GoRecentsActivityRootView extends BaseDragLayer<RecentsActivity> {
-    public GoRecentsActivityRootView(Context context, AttributeSet attrs) {
-        super(context, attrs, 1 /* alphaChannelCount */);
-        // Go leaves touch control to the view itself.
-        mControllers = new TouchController[0];
-        setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
-                | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
-                | SYSTEM_UI_FLAG_LAYOUT_STABLE);
-    }
-
-    @Override
-    public void setInsets(Rect insets) {
-        if (insets.equals(mInsets)) {
-            return;
-        }
-        super.setInsets(insets);
-    }
-
-    @Override
-    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        Insets sysInsets = insets.getSystemWindowInsets();
-        setInsets(new Rect(sysInsets.left, sysInsets.top, sysInsets.right, sysInsets.bottom));
-        return insets.consumeSystemWindowInsets();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/util/ShelfPeekAnim.java b/go/quickstep/src/com/android/quickstep/util/ShelfPeekAnim.java
deleted file mode 100644
index 3842efb..0000000
--- a/go/quickstep/src/com/android/quickstep/util/ShelfPeekAnim.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.util;
-
-import com.android.launcher3.Launcher;
-
-/** Empty class, only exists so that lowRamWithQuickstepIconRecentsDebug compiles. */
-public class ShelfPeekAnim {
-    public ShelfPeekAnim(Launcher launcher) {
-    }
-
-    public enum ShelfAnimState {
-    }
-
-    public boolean isPeeking() {
-        return false;
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
deleted file mode 100644
index e380698..0000000
--- a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
+++ /dev/null
@@ -1,962 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.views;
-
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-
-import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
-
-import static com.android.launcher3.anim.Interpolators.ACCEL_2;
-import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
-import static com.android.quickstep.TaskAdapter.ITEM_TYPE_CLEAR_ALL;
-import static com.android.quickstep.TaskAdapter.ITEM_TYPE_TASK;
-import static com.android.quickstep.TaskAdapter.MAX_TASKS_TO_DISPLAY;
-import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
-import static com.android.quickstep.util.RemoteAnimationProvider.getLayer;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Matrix;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.Drawable;
-import android.os.UserHandle;
-import android.util.ArraySet;
-import android.util.AttributeSet;
-import android.util.FloatProperty;
-import android.view.View;
-import android.view.ViewDebug;
-import android.view.ViewTreeObserver;
-import android.view.animation.PathInterpolator;
-import android.widget.FrameLayout;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.interpolator.view.animation.LinearOutSlowInInterpolator;
-import androidx.recyclerview.widget.DefaultItemAnimator;
-import androidx.recyclerview.widget.ItemTouchHelper;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
-import androidx.recyclerview.widget.RecyclerView.ItemDecoration;
-import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
-
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.Insettable;
-import com.android.launcher3.R;
-import com.android.launcher3.util.Themes;
-import com.android.quickstep.ContentFillItemAnimator;
-import com.android.quickstep.RecentsModel;
-import com.android.quickstep.RecentsModel.TaskVisualsChangeListener;
-import com.android.quickstep.RecentsToActivityHelper;
-import com.android.quickstep.TaskActionController;
-import com.android.quickstep.TaskAdapter;
-import com.android.quickstep.TaskHolder;
-import com.android.quickstep.TaskListLoader;
-import com.android.quickstep.TaskSwipeCallback;
-import com.android.quickstep.util.MultiValueUpdateListener;
-import com.android.systemui.shared.recents.model.Task;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
-import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Root view for the icon recents view. Acts as the main interface to the rest of the Launcher code
- * base.
- */
-public final class IconRecentsView extends FrameLayout
-        implements Insettable, TaskVisualsChangeListener {
-
-    public static final FloatProperty<IconRecentsView> CONTENT_ALPHA =
-            new FloatProperty<IconRecentsView>("contentAlpha") {
-                @Override
-                public void setValue(IconRecentsView view, float v) {
-                    ALPHA.set(view, v);
-                    if (view.getVisibility() != VISIBLE && v > 0) {
-                        view.setVisibility(VISIBLE);
-                    } else if (view.getVisibility() != GONE && v == 0){
-                        view.setVisibility(GONE);
-                    }
-                }
-
-                @Override
-                public Float get(IconRecentsView view) {
-                    return ALPHA.get(view);
-                }
-            };
-    private static final long CROSSFADE_DURATION = 300;
-    private static final long LAYOUT_ITEM_ANIMATE_IN_DURATION = 150;
-    private static final long LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN = 40;
-    private static final long ITEM_ANIMATE_OUT_DURATION = 150;
-    private static final long ITEM_ANIMATE_OUT_DELAY_BETWEEN = 40;
-    private static final float ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO = .25f;
-    private static final long CLEAR_ALL_FADE_DELAY = 120;
-
-    private static final long REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION = 300;
-    private static final long REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION = 400;
-    private static final long REMOTE_TO_RECENTS_ITEM_FADE_START_DELAY = 200;
-    private static final long REMOTE_TO_RECENTS_ITEM_FADE_DURATION = 217;
-    private static final long REMOTE_TO_RECENTS_ITEM_FADE_BETWEEN_DELAY = 33;
-
-    private static final PathInterpolator FAST_OUT_SLOW_IN_1 =
-            new PathInterpolator(.4f, 0f, 0f, 1f);
-    private static final PathInterpolator FAST_OUT_SLOW_IN_2 =
-            new PathInterpolator(.5f, 0f, 0f, 1f);
-    private static final LinearOutSlowInInterpolator OUT_SLOW_IN =
-            new LinearOutSlowInInterpolator();
-
-    public static final long REMOTE_APP_TO_OVERVIEW_DURATION =
-            REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION;
-
-    /**
-     * A ratio representing the view's relative placement within its padded space. For example, 0
-     * is top aligned and 0.5 is centered vertically.
-     */
-    @ViewDebug.ExportedProperty(category = "launcher")
-
-    private final Context mContext;
-    private final TaskListLoader mTaskLoader;
-    private final TaskAdapter mTaskAdapter;
-    private final LinearLayoutManager mTaskLayoutManager;
-    private final TaskActionController mTaskActionController;
-    private final DefaultItemAnimator mDefaultItemAnimator = new DefaultItemAnimator();
-    private final ContentFillItemAnimator mLoadingContentItemAnimator =
-            new ContentFillItemAnimator();
-    private final BaseActivity mActivity;
-    private final Drawable mStatusBarForegroundScrim;
-
-    private RecentsToActivityHelper mActivityHelper;
-    private RecyclerView mTaskRecyclerView;
-    private View mShowingContentView;
-    private View mEmptyView;
-    private View mContentView;
-    private boolean mTransitionedFromApp;
-    private boolean mUsingRemoteAnimation;
-    private boolean mStartedEnterAnimation;
-    private boolean mShowStatusBarForegroundScrim;
-    private AnimatorSet mLayoutAnimation;
-    private final ArraySet<View> mLayingOutViews = new ArraySet<>();
-    private Rect mInsets;
-
-    public IconRecentsView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        mActivity = BaseActivity.fromContext(context);
-        mContext = context;
-        mStatusBarForegroundScrim  =
-                Themes.getAttrDrawable(mContext, R.attr.workspaceStatusBarScrim);
-        mTaskLoader = new TaskListLoader(mContext);
-        mTaskAdapter = new TaskAdapter(mTaskLoader);
-        mTaskAdapter.setOnClearAllClickListener(view -> animateClearAllTasks());
-        mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter,
-                mActivity.getStatsLogManager());
-        mTaskAdapter.setActionController(mTaskActionController);
-        mTaskLayoutManager = new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */);
-    }
-
-    @Override
-    public Task onTaskThumbnailChanged(int taskId, ThumbnailData thumbnailData) {
-        ArrayList<TaskItemView> itemViews = getTaskViews();
-        for (int i = 0, size = itemViews.size(); i < size; i++) {
-            TaskItemView taskView = itemViews.get(i);
-            TaskHolder taskHolder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
-            Optional<Task> optTask = taskHolder.getTask();
-            if (optTask.filter(task -> task.key.id == taskId).isPresent()) {
-                Task task = optTask.get();
-                // Update thumbnail on the task.
-                task.thumbnail = thumbnailData;
-                taskView.setThumbnail(thumbnailData);
-                return task;
-            }
-        }
-        return null;
-    }
-
-    @Override
-    public void onTaskIconChanged(String pkg, UserHandle user) { }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        if (mTaskRecyclerView == null) {
-            mTaskRecyclerView = findViewById(R.id.recent_task_recycler_view);
-            mTaskRecyclerView.setAdapter(mTaskAdapter);
-            mTaskRecyclerView.setLayoutManager(mTaskLayoutManager);
-            ItemTouchHelper helper = new ItemTouchHelper(
-                    new TaskSwipeCallback(holder -> {
-                        mTaskActionController.removeTask(holder);
-                        if (mTaskLoader.getCurrentTaskList().isEmpty()) {
-                            mActivityHelper.leaveRecents();
-                        }
-                    }));
-            helper.attachToRecyclerView(mTaskRecyclerView);
-            mTaskRecyclerView.addOnChildAttachStateChangeListener(
-                    new OnChildAttachStateChangeListener() {
-                        @Override
-                        public void onChildViewAttachedToWindow(@NonNull View view) {
-                            if (mLayoutAnimation != null && !mLayingOutViews.contains(view)) {
-                                // Child view was added that is not part of current layout animation
-                                // so restart the animation.
-                                animateFadeInLayoutAnimation();
-                            }
-                        }
-
-                        @Override
-                        public void onChildViewDetachedFromWindow(@NonNull View view) { }
-                    });
-            mTaskRecyclerView.setItemAnimator(mDefaultItemAnimator);
-            mLoadingContentItemAnimator.setOnAnimationFinishedRunnable(
-                    () -> mTaskRecyclerView.setItemAnimator(new DefaultItemAnimator()));
-            ItemDecoration marginDecorator = new ItemDecoration() {
-                @Override
-                public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
-                        @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
-                    // TODO: Determine if current margins cause off screen item to be fully off
-                    // screen and if so, modify them so that it is partially off screen.
-                    int itemType = parent.getChildViewHolder(view).getItemViewType();
-                    Resources res = getResources();
-                    switch (itemType) {
-                        case ITEM_TYPE_CLEAR_ALL:
-                            outRect.top = (int) res.getDimension(
-                                    R.dimen.clear_all_item_view_top_margin);
-                            outRect.bottom = (int) res.getDimension(
-                                    R.dimen.clear_all_item_view_bottom_margin);
-                            break;
-                        case ITEM_TYPE_TASK:
-                            int desiredTopMargin = (int) res.getDimension(
-                                    R.dimen.task_item_top_margin);
-                            if (mTaskRecyclerView.getChildAdapterPosition(view) ==
-                                    state.getItemCount() - 1) {
-                                // Only add top margin to top task view if insets aren't enough.
-                                if (mInsets.top < desiredTopMargin) {
-                                    outRect.top = desiredTopMargin - mInsets.bottom;
-                                }
-                                return;
-                            }
-                            outRect.top = desiredTopMargin;
-                            break;
-                        default:
-                    }
-                }
-            };
-            mTaskRecyclerView.addItemDecoration(marginDecorator);
-
-            mEmptyView = findViewById(R.id.recent_task_empty_view);
-            mContentView = mTaskRecyclerView;
-            mTaskAdapter.registerAdapterDataObserver(new AdapterDataObserver() {
-                @Override
-                public void onChanged() {
-                    updateContentViewVisibility();
-                }
-
-                @Override
-                public void onItemRangeRemoved(int positionStart, int itemCount) {
-                    updateContentViewVisibility();
-                }
-            });
-        }
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        RecentsModel.INSTANCE.get(getContext()).addThumbnailChangeListener(this);
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        RecentsModel.INSTANCE.get(getContext()).removeThumbnailChangeListener(this);
-    }
-
-    @Override
-    public void setEnabled(boolean enabled) {
-        super.setEnabled(enabled);
-        int childCount = mTaskRecyclerView.getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            mTaskRecyclerView.getChildAt(i).setEnabled(enabled);
-        }
-    }
-
-    /**
-     * Set activity helper for the view to callback to.
-     *
-     * @param helper the activity helper
-     */
-    public void setRecentsToActivityHelper(@NonNull RecentsToActivityHelper helper) {
-        mActivityHelper = helper;
-    }
-
-    /**
-     * Logic for when we know we are going to overview/recents and will be putting up the recents
-     * view. This should be used to prepare recents (e.g. load any task data, etc.) before it
-     * becomes visible.
-     */
-    public void onBeginTransitionToOverview() {
-        mStartedEnterAnimation = false;
-        if (mContext.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
-            // Scroll to bottom of task in landscape mode. This is a non-issue in portrait mode as
-            // all tasks should be visible to fill up the screen in portrait mode and the view will
-            // not be scrollable.
-            mTaskLayoutManager.scrollToPositionWithOffset(TASKS_START_POSITION, 0 /* offset */);
-        }
-        if (!mUsingRemoteAnimation) {
-            scheduleFadeInLayoutAnimation();
-        }
-        // Load any task changes
-        if (!mTaskLoader.needsToLoad()) {
-            return;
-        }
-        mTaskAdapter.setIsShowingLoadingUi(true);
-        mTaskAdapter.notifyDataSetChanged();
-        mTaskLoader.loadTaskList(tasks -> {
-            int numEmptyItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
-            mTaskAdapter.setIsShowingLoadingUi(false);
-            int numActualItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
-            if (numEmptyItems < numActualItems) {
-                throw new IllegalStateException("There are less empty item views than the number "
-                        + "of items to animate to.");
-            }
-            // Possible that task list loads faster than adapter changes propagate to layout so
-            // only start content fill animation if there aren't any pending adapter changes and
-            // we've started the on enter layout animation.
-            boolean needsContentFillAnimation =
-                    !mTaskRecyclerView.hasPendingAdapterUpdates() && mStartedEnterAnimation;
-            if (needsContentFillAnimation) {
-                // Set item animator for content filling animation. The item animator will switch
-                // back to the default on completion
-                mTaskRecyclerView.setItemAnimator(mLoadingContentItemAnimator);
-                mTaskAdapter.notifyItemRangeRemoved(TASKS_START_POSITION + numActualItems,
-                        numEmptyItems - numActualItems);
-                mTaskAdapter.notifyItemRangeChanged(TASKS_START_POSITION, numActualItems,
-                        CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT);
-            } else {
-                // Notify change without animating.
-                mTaskAdapter.notifyDataSetChanged();
-            }
-        });
-    }
-
-    /**
-     * Set whether we transitioned to recents from the most recent app.
-     *
-     * @param transitionedFromApp true if transitioned from the most recent app, false otherwise
-     */
-    public void setTransitionedFromApp(boolean transitionedFromApp) {
-        mTransitionedFromApp = transitionedFromApp;
-    }
-
-    /**
-     * Set whether we're using a custom remote animation. If so, we will not do the default layout
-     * animation when entering recents and instead wait for the remote app surface to be ready to
-     * use.
-     *
-     * @param usingRemoteAnimation true if doing a remote animation, false o/w
-     */
-    public void setUsingRemoteAnimation(boolean usingRemoteAnimation) {
-        mUsingRemoteAnimation = usingRemoteAnimation;
-    }
-
-    /**
-     * Handles input from the overview button. Launch the most recent task unless we just came from
-     * the app. In that case, we launch the next most recent.
-     */
-    public void handleOverviewCommand() {
-        List<Task> tasks = mTaskLoader.getCurrentTaskList();
-        int tasksSize = tasks.size();
-        if (tasksSize == 0) {
-            // Do nothing
-            return;
-        }
-        Task taskToLaunch;
-        if (mTransitionedFromApp && tasksSize > 1) {
-            // Launch the next most recent app
-            taskToLaunch = tasks.get(1);
-        } else {
-            // Launch the most recent app
-            taskToLaunch = tasks.get(0);
-        }
-
-        // See if view for this task is attached, and if so, animate launch from that view.
-        ArrayList<TaskItemView> itemViews = getTaskViews();
-        for (int i = 0, size = itemViews.size(); i < size; i++) {
-            TaskItemView taskView = itemViews.get(i);
-            TaskHolder holder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
-            if (Objects.equals(holder.getTask(), Optional.of(taskToLaunch))) {
-                mTaskActionController.launchTaskFromView(holder);
-                return;
-            }
-        }
-
-        // Otherwise, just use a basic launch animation.
-        mTaskActionController.launchTask(taskToLaunch);
-    }
-
-    /**
-     * Set whether or not to show the scrim in between the view and the top insets. This only works
-     * if the view is being insetted in the first place.
-     *
-     * The scrim is added to the activity's root view to prevent animations on this view
-     * affecting the scrim. As a result, it is the activity's responsibility to show/hide this
-     * scrim as appropriate.
-     *
-     * @param showStatusBarForegroundScrim true to show the scrim, false to hide
-     */
-    public void setShowStatusBarForegroundScrim(boolean showStatusBarForegroundScrim) {
-        mShowStatusBarForegroundScrim = showStatusBarForegroundScrim;
-        if (mShowStatusBarForegroundScrim != showStatusBarForegroundScrim) {
-            updateStatusBarScrim();
-        }
-    }
-
-    private void updateStatusBarScrim() {
-        boolean shouldShow = mInsets.top != 0 && mShowStatusBarForegroundScrim;
-        mActivity.getDragLayer().setForeground(shouldShow ? mStatusBarForegroundScrim : null);
-    }
-
-    /**
-     * Get the bottom most task view to animate to.
-     *
-     * @return the task view
-     */
-    private @Nullable TaskItemView getBottomTaskView() {
-        int childCount = mTaskRecyclerView.getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            View view = mTaskRecyclerView.getChildAt(i);
-            if (mTaskRecyclerView.getChildViewHolder(view).getItemViewType() == ITEM_TYPE_TASK) {
-                return (TaskItemView) view;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Whether this view has processed all data changes and is ready to animate from the app to
-     * the overview.
-     *
-     * @return true if ready to animate app to overview, false otherwise
-     */
-    public boolean isReadyForRemoteAnim() {
-        return !mTaskRecyclerView.hasPendingAdapterUpdates();
-    }
-
-    /**
-     * Set a callback for whenever this view is ready to do a remote animation from the app to
-     * overview. See {@link #isReadyForRemoteAnim()}.
-     *
-     * @param callback callback to run when view is ready to animate
-     */
-    public void setOnReadyForRemoteAnimCallback(onReadyForRemoteAnimCallback callback) {
-        mTaskRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
-                new ViewTreeObserver.OnGlobalLayoutListener() {
-                    @Override
-                    public void onGlobalLayout() {
-                        if (isReadyForRemoteAnim()) {
-                            callback.onReadyForRemoteAnim();
-                            mTaskRecyclerView.getViewTreeObserver().
-                                    removeOnGlobalLayoutListener(this);
-                        }
-                    }
-                });
-    }
-
-    /**
-     * Clear all tasks and animate out.
-     */
-    private void animateClearAllTasks() {
-        setEnabled(false);
-        ArrayList<TaskItemView> itemViews = getTaskViews();
-
-        AnimatorSet clearAnim = new AnimatorSet();
-        long currentDelay = 0;
-
-        // Animate each item view to the right and fade out.
-        for (int i = 0, size = itemViews.size(); i < size; i++) {
-            TaskItemView itemView = itemViews.get(i);
-            PropertyValuesHolder transXproperty = PropertyValuesHolder.ofFloat(TRANSLATION_X,
-                    0, itemView.getWidth() * ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO);
-            PropertyValuesHolder alphaProperty = PropertyValuesHolder.ofFloat(ALPHA, 1.0f, 0f);
-            ObjectAnimator itemAnim = ObjectAnimator.ofPropertyValuesHolder(itemView,
-                    transXproperty, alphaProperty);
-            itemAnim.setDuration(ITEM_ANIMATE_OUT_DURATION);
-            itemAnim.setStartDelay(currentDelay);
-
-            clearAnim.play(itemAnim);
-            currentDelay += ITEM_ANIMATE_OUT_DELAY_BETWEEN;
-        }
-
-        // Animate view fading and leave recents when faded enough.
-        ValueAnimator contentAlpha = ValueAnimator.ofFloat(1.0f, 0f)
-                .setDuration(CROSSFADE_DURATION);
-        contentAlpha.setStartDelay(CLEAR_ALL_FADE_DELAY);
-        contentAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-            private boolean mLeftRecents = false;
-
-            @Override
-            public void onAnimationUpdate(ValueAnimator valueAnimator) {
-                mContentView.setAlpha((float) valueAnimator.getAnimatedValue());
-                // Leave recents while fading out.
-                if ((float) valueAnimator.getAnimatedValue() < .5f && !mLeftRecents) {
-                    mActivityHelper.leaveRecents();
-                    mLeftRecents = true;
-                }
-            }
-        });
-
-        clearAnim.play(contentAlpha);
-        clearAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                for (int i = 0, size = itemViews.size(); i < size; i++) {
-                    TaskItemView itemView = itemViews.get(i);
-                    itemView.setTranslationX(0);
-                    itemView.setAlpha(1.0f);
-                }
-                setEnabled(true);
-                mContentView.setVisibility(GONE);
-                mTaskActionController.clearAllTasks();
-            }
-        });
-        clearAnim.start();
-    }
-
-    /**
-     * Get attached task item views ordered by most recent.
-     *
-     * @return array list of attached task item views
-     */
-    private ArrayList<TaskItemView> getTaskViews() {
-        int taskCount = mTaskRecyclerView.getChildCount();
-        ArrayList<TaskItemView> itemViews = new ArrayList<>();
-        for (int i = 0; i < taskCount; i ++) {
-            View child = mTaskRecyclerView.getChildAt(i);
-            if (child instanceof TaskItemView) {
-                itemViews.add((TaskItemView) child);
-            }
-        }
-        return itemViews;
-    }
-
-    /**
-     * Update the content view so that the appropriate view is shown based off the current list
-     * of tasks.
-     */
-    private void updateContentViewVisibility() {
-        int taskListSize = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
-        if (mShowingContentView != mEmptyView && taskListSize == 0) {
-            mShowingContentView = mEmptyView;
-            crossfadeViews(mEmptyView, mContentView);
-        }
-        if (mShowingContentView != mContentView && taskListSize > 0) {
-            mShowingContentView = mContentView;
-            crossfadeViews(mContentView, mEmptyView);
-        }
-    }
-
-    /**
-     * Animate views so that one view fades in while the other fades out.
-     *
-     * @param fadeInView view that should fade in
-     * @param fadeOutView view that should fade out
-     */
-    private void crossfadeViews(View fadeInView, View fadeOutView) {
-        fadeInView.animate().cancel();
-        fadeInView.setVisibility(VISIBLE);
-        fadeInView.setAlpha(0f);
-        fadeInView.animate()
-                .alpha(1f)
-                .setDuration(CROSSFADE_DURATION)
-                .setListener(null);
-
-        fadeOutView.animate().cancel();
-        fadeOutView.animate()
-                .alpha(0f)
-                .setDuration(CROSSFADE_DURATION)
-                .setListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        fadeOutView.setVisibility(GONE);
-                    }
-                });
-    }
-
-    /**
-     * Schedule a one-shot layout animation on the next layout. Separate from
-     * {@link #scheduleLayoutAnimation()} as the animation is {@link Animator} based and acts on the
-     * view properties themselves, allowing more controllable behavior and making it easier to
-     * manage when the animation conflicts with another animation.
-     */
-    private void scheduleFadeInLayoutAnimation() {
-        mTaskRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
-                new ViewTreeObserver.OnGlobalLayoutListener() {
-                    @Override
-                    public void onGlobalLayout() {
-                        animateFadeInLayoutAnimation();
-                        mTaskRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
-                    }
-                });
-    }
-
-    /**
-     * Start animating the layout animation where items fade in.
-     */
-    private void animateFadeInLayoutAnimation() {
-        if (mLayoutAnimation != null) {
-            // If layout animation still in progress, cancel and restart.
-            mLayoutAnimation.cancel();
-        }
-        ArrayList<TaskItemView> views = getTaskViews();
-        int delay = 0;
-        mLayoutAnimation = new AnimatorSet();
-        for (int i = 0, size = views.size(); i < size; i++) {
-            TaskItemView view = views.get(i);
-            view.setAlpha(0.0f);
-            Animator alphaAnim = ObjectAnimator.ofFloat(view, ALPHA, 0.0f, 1.0f);
-            alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION).setStartDelay(delay);
-            alphaAnim.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    view.setAlpha(1.0f);
-                    mLayingOutViews.remove(view);
-                }
-            });
-            delay += LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN;
-            mLayoutAnimation.play(alphaAnim);
-            mLayingOutViews.add(view);
-        }
-        mLayoutAnimation.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mLayoutAnimation = null;
-            }
-        });
-        mLayoutAnimation.start();
-        mStartedEnterAnimation = true;
-    }
-
-    /**
-     * Play remote app to recents animation when the app is the home activity. We use a simple
-     * cross-fade here. Note this is only used if the home activity is a separate app than the
-     * recents activity.
-     *
-     * @param anim animator set
-     * @param homeTarget the home surface thats closing
-     * @param recentsTarget the surface containing recents
-     */
-    public void playRemoteHomeToRecentsAnimation(@NonNull AnimatorSet anim,
-            @NonNull RemoteAnimationTargetCompat homeTarget,
-            @NonNull RemoteAnimationTargetCompat recentsTarget) {
-        SyncRtSurfaceTransactionApplierCompat surfaceApplier =
-                new SyncRtSurfaceTransactionApplierCompat(this);
-
-        SurfaceParams[] params = new SurfaceParams[2];
-        int boostedMode = MODE_CLOSING;
-
-        ValueAnimator remoteHomeAnim = ValueAnimator.ofFloat(0, 1);
-        remoteHomeAnim.setDuration(REMOTE_APP_TO_OVERVIEW_DURATION);
-
-        remoteHomeAnim.addUpdateListener(valueAnimator -> {
-            float val = (float) valueAnimator.getAnimatedValue();
-            float alpha;
-            RemoteAnimationTargetCompat visibleTarget;
-            RemoteAnimationTargetCompat invisibleTarget;
-            if (val < .5f) {
-                visibleTarget = homeTarget;
-                invisibleTarget = recentsTarget;
-                alpha = 1 - (val * 2);
-            } else {
-                visibleTarget = recentsTarget;
-                invisibleTarget = homeTarget;
-                alpha = (val - .5f) * 2;
-            }
-            params[0] = new SurfaceParams(visibleTarget.leash, alpha, null /* matrix */,
-                    null /* windowCrop */, getLayer(visibleTarget, boostedMode),
-                    0 /* cornerRadius */);
-            params[1] = new SurfaceParams(invisibleTarget.leash, 0.0f, null /* matrix */,
-                    null /* windowCrop */, getLayer(invisibleTarget, boostedMode),
-                    0 /* cornerRadius */);
-            surfaceApplier.scheduleApply(params);
-        });
-        anim.play(remoteHomeAnim);
-        animateFadeInLayoutAnimation();
-    }
-
-    /**
-     * Play remote animation from app to recents. This should scale the currently closing app down
-     * to the recents thumbnail.
-     *
-     * @param anim animator set
-     * @param appTarget the app surface thats closing
-     * @param recentsTarget the surface containing recents
-     */
-    public void playRemoteAppToRecentsAnimation(@NonNull AnimatorSet anim,
-            @NonNull RemoteAnimationTargetCompat appTarget,
-            @NonNull RemoteAnimationTargetCompat recentsTarget) {
-        TaskItemView bottomView = getBottomTaskView();
-        if (bottomView == null) {
-            // This can be null if there were previously 0 tasks and the recycler view has not had
-            // enough time to take in the data change, bind a new view, and lay out the new view.
-            // TODO: Have a fallback to animate to
-            anim.play(ValueAnimator.ofInt(0, 1).setDuration(REMOTE_APP_TO_OVERVIEW_DURATION));
-            return;
-        }
-        final Matrix appMatrix = new Matrix();
-        playRemoteTransYAnim(anim, appMatrix);
-        playRemoteAppScaleDownAnim(anim, appMatrix, appTarget, recentsTarget,
-                bottomView.getThumbnailView());
-        playRemoteTaskListFadeIn(anim, bottomView);
-        mStartedEnterAnimation = true;
-    }
-
-    /**
-     * Play translation Y animation for the remote app to recents animation. Animates over all task
-     * views as well as the closing app, easing them into their final vertical positions.
-     *
-     * @param anim animator set to play on
-     * @param appMatrix transformation matrix for the closing app surface
-     */
-    private void playRemoteTransYAnim(@NonNull AnimatorSet anim, @NonNull Matrix appMatrix) {
-        final ArrayList<TaskItemView> views = getTaskViews();
-
-        // Start Y translation from about halfway through the tasks list to the bottom thumbnail.
-        float taskHeight = getResources().getDimension(R.dimen.task_item_height);
-        float totalTransY = -(MAX_TASKS_TO_DISPLAY / 2.0f - 1) * taskHeight;
-        for (int i = 0, size = views.size(); i < size; i++) {
-            views.get(i).setTranslationY(totalTransY);
-        }
-
-        ValueAnimator transYAnim = ValueAnimator.ofFloat(totalTransY, 0);
-        transYAnim.setDuration(REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION);
-        transYAnim.setInterpolator(FAST_OUT_SLOW_IN_2);
-        transYAnim.addUpdateListener(valueAnimator -> {
-            float transY = (float) valueAnimator.getAnimatedValue();
-            for (int i = 0, size = views.size(); i < size; i++) {
-                views.get(i).setTranslationY(transY);
-            }
-            appMatrix.postTranslate(0, transY - totalTransY);
-        });
-        transYAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                for (int i = 0, size = views.size(); i < size; i++) {
-                    views.get(i).setTranslationY(0);
-                }
-            }
-        });
-        anim.play(transYAnim);
-    }
-
-    /**
-     * Play the scale down animation for the remote app to recents animation where the app surface
-     * scales down to where the thumbnail is.
-     *
-     * @param anim animator set to play on
-     * @param appMatrix transformation matrix for the app surface
-     * @param appTarget closing app target
-     * @param recentsTarget opening recents target
-     * @param thumbnailView thumbnail view to animate to
-     */
-    private void playRemoteAppScaleDownAnim(@NonNull AnimatorSet anim, @NonNull Matrix appMatrix,
-            @NonNull RemoteAnimationTargetCompat appTarget,
-            @NonNull RemoteAnimationTargetCompat recentsTarget,
-            @NonNull View thumbnailView) {
-        // Identify where the entering remote app should animate to.
-        Rect endRect = new Rect();
-        thumbnailView.getGlobalVisibleRect(endRect);
-        Rect appBounds = appTarget.sourceContainerBounds;
-        RectF currentAppRect = new RectF();
-
-        SyncRtSurfaceTransactionApplierCompat surfaceApplier =
-                new SyncRtSurfaceTransactionApplierCompat(this);
-
-        // Keep recents visible throughout the animation.
-        SurfaceParams[] params = new SurfaceParams[2];
-        // Closing app should stay on top.
-        int boostedMode = MODE_CLOSING;
-        params[0] = new SurfaceParams(recentsTarget.leash, 1f, null /* matrix */,
-                null /* windowCrop */, getLayer(recentsTarget, boostedMode), 0 /* cornerRadius */);
-
-        ValueAnimator remoteAppAnim = ValueAnimator.ofInt(0, 1);
-        remoteAppAnim.setDuration(REMOTE_TO_RECENTS_VERTICAL_EASE_IN_DURATION);
-        remoteAppAnim.addUpdateListener(new MultiValueUpdateListener() {
-            private final FloatProp mScaleX;
-            private final FloatProp mScaleY;
-            private final FloatProp mTranslationX;
-            private final FloatProp mTranslationY;
-            private final FloatProp mAlpha;
-
-            {
-                // Scale down and move to view location.
-                float endScaleX = ((float) endRect.width()) / appBounds.width();
-                mScaleX = new FloatProp(1f, endScaleX, 0, REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION,
-                        FAST_OUT_SLOW_IN_1);
-                float endScaleY = ((float) endRect.height()) / appBounds.height();
-                mScaleY = new FloatProp(1f, endScaleY, 0, REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION,
-                        FAST_OUT_SLOW_IN_1);
-                float endTranslationX = endRect.left -
-                        (appBounds.width() - thumbnailView.getWidth()) / 2.0f;
-                mTranslationX = new FloatProp(0, endTranslationX, 0,
-                        REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION, FAST_OUT_SLOW_IN_1);
-                float endTranslationY = endRect.top -
-                        (appBounds.height() - thumbnailView.getHeight()) / 2.0f;
-                mTranslationY = new FloatProp(0, endTranslationY, 0,
-                        REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION, FAST_OUT_SLOW_IN_2);
-                mAlpha = new FloatProp(1.0f, 0, 0, REMOTE_TO_RECENTS_APP_SCALE_DOWN_DURATION,
-                        ACCEL_2);
-            }
-
-            @Override
-            public void onUpdate(float percent) {
-                Matrix m = new Matrix();
-                m.preScale(mScaleX.value, mScaleY.value,
-                        appBounds.width() / 2.0f, appBounds.height() / 2.0f);
-                m.postTranslate(mTranslationX.value, mTranslationY.value);
-                appMatrix.preConcat(m);
-                params[1] = new SurfaceParams(appTarget.leash, mAlpha.value, appMatrix,
-                        null /* windowCrop */, getLayer(appTarget, boostedMode),
-                        0 /* cornerRadius */);
-                surfaceApplier.scheduleApply(params);
-
-                m.mapRect(currentAppRect, new RectF(appBounds));
-                setViewToRect(thumbnailView, new RectF(endRect), currentAppRect);
-                appMatrix.reset();
-            }
-        });
-        remoteAppAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                thumbnailView.setTranslationY(0);
-                thumbnailView.setTranslationX(0);
-                thumbnailView.setScaleX(1);
-                thumbnailView.setScaleY(1);
-            }
-        });
-        anim.play(remoteAppAnim);
-    }
-
-    /**
-     * Play task list fade in animation as part of remote app to recents animation. This animation
-     * ensures that the task views in the recents list fade in from bottom to top.
-     *
-     * @param anim animator set to play on
-     * @param appTaskView the task view associated with the remote app closing
-     */
-    private void playRemoteTaskListFadeIn(@NonNull AnimatorSet anim,
-            @NonNull TaskItemView appTaskView) {
-        long delay = REMOTE_TO_RECENTS_ITEM_FADE_START_DELAY;
-        int childCount = mTaskRecyclerView.getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            ValueAnimator fadeAnim = ValueAnimator.ofFloat(0, 1.0f);
-            fadeAnim.setDuration(REMOTE_TO_RECENTS_ITEM_FADE_DURATION).setInterpolator(OUT_SLOW_IN);
-            fadeAnim.setStartDelay(delay);
-            View view = mTaskRecyclerView.getChildAt(i);
-            if (Objects.equals(view, appTaskView)) {
-                // Only animate icon and text for the view with snapshot animating in
-                final View icon = appTaskView.getIconView();
-                final View label = appTaskView.getLabelView();
-
-                icon.setAlpha(0.0f);
-                label.setAlpha(0.0f);
-
-                fadeAnim.addUpdateListener(alphaVal -> {
-                    float val = alphaVal.getAnimatedFraction();
-
-                    icon.setAlpha(val);
-                    label.setAlpha(val);
-                });
-                fadeAnim.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        icon.setAlpha(1.0f);
-                        label.setAlpha(1.0f);
-                    }
-                });
-            } else {
-                // Otherwise, fade in the entire view.
-                view.setAlpha(0.0f);
-                fadeAnim.addUpdateListener(alphaVal -> {
-                    float val = alphaVal.getAnimatedFraction();
-                    view.setAlpha(val);
-                });
-                fadeAnim.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        view.setAlpha(1.0f);
-                    }
-                });
-            }
-            anim.play(fadeAnim);
-
-            int itemType = mTaskRecyclerView.getChildViewHolder(view).getItemViewType();
-            if (itemType == ITEM_TYPE_CLEAR_ALL) {
-                // Don't add delay. Clear all should animate at same time as next view.
-                continue;
-            }
-            delay += REMOTE_TO_RECENTS_ITEM_FADE_BETWEEN_DELAY;
-        }
-    }
-
-    /**
-     * Set view properties so that the view fits to the target rect.
-     *
-     * @param view view to set
-     * @param origRect original rect that view was located
-     * @param targetRect rect to set to
-     */
-    private void setViewToRect(View view, RectF origRect, RectF targetRect) {
-        float dX = targetRect.left - origRect.left;
-        float dY = targetRect.top - origRect.top;
-        view.setTranslationX(dX);
-        view.setTranslationY(dY);
-
-        float scaleX = targetRect.width() / origRect.width();
-        float scaleY = targetRect.height() / origRect.height();
-        view.setPivotX(0);
-        view.setPivotY(0);
-        view.setScaleX(scaleX);
-        view.setScaleY(scaleY);
-    }
-
-    @Override
-    public void setInsets(Rect insets) {
-        mInsets = insets;
-        mTaskRecyclerView.setPadding(insets.left, insets.top, insets.right, insets.bottom);
-        mTaskRecyclerView.invalidateItemDecorations();
-        if (mInsets.top != 0) {
-            updateStatusBarScrim();
-        }
-    }
-
-    /**
-     * Callback for when this view is ready for a remote animation from app to overview.
-     */
-    public interface onReadyForRemoteAnimCallback {
-
-        void onReadyForRemoteAnim();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
deleted file mode 100644
index f184ad0..0000000
--- a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.views;
-
-import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.util.FloatProperty;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.R;
-import com.android.quickstep.ThumbnailDrawable;
-import com.android.systemui.shared.recents.model.ThumbnailData;
-
-/**
- * View representing an individual task item with the icon + thumbnail adjacent to the task label.
- */
-public final class TaskItemView extends LinearLayout {
-
-    private static final String EMPTY_LABEL = "";
-    private static final String DEFAULT_LABEL = "...";
-    private final Drawable mDefaultIcon;
-    private final Drawable mDefaultThumbnail;
-    private final TaskLayerDrawable mIconDrawable;
-    private final TaskLayerDrawable mThumbnailDrawable;
-    private View mTaskIconThumbnailView;
-    private TextView mLabelView;
-    private ImageView mIconView;
-    private ImageView mThumbnailView;
-    private float mContentTransitionProgress;
-    private int mDisplayedOrientation;
-
-    /**
-     * Property representing the content transition progress of the view. 1.0f represents that the
-     * currently bound icon, thumbnail, and label are fully animated in and visible.
-     */
-    public static FloatProperty CONTENT_TRANSITION_PROGRESS =
-            new FloatProperty<TaskItemView>("taskContentTransitionProgress") {
-                @Override
-                public void setValue(TaskItemView view, float progress) {
-                    view.setContentTransitionProgress(progress);
-                }
-
-                @Override
-                public Float get(TaskItemView view) {
-                    return view.mContentTransitionProgress;
-                }
-            };
-
-    public TaskItemView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        Resources res = context.getResources();
-        mDefaultIcon = res.getDrawable(android.R.drawable.sym_def_app_icon, context.getTheme());
-        mDefaultThumbnail = res.getDrawable(R.drawable.default_thumbnail, context.getTheme());
-        mIconDrawable = new TaskLayerDrawable(context);
-        mThumbnailDrawable = new TaskLayerDrawable(context);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mLabelView = findViewById(R.id.task_label);
-        mTaskIconThumbnailView = findViewById(R.id.task_icon_and_thumbnail);
-        mThumbnailView = findViewById(R.id.task_thumbnail);
-        mIconView = findViewById(R.id.task_icon);
-
-        mThumbnailView.setImageDrawable(mThumbnailDrawable);
-        mIconView.setImageDrawable(mIconDrawable);
-
-        resetToEmptyUi();
-        CONTENT_TRANSITION_PROGRESS.setValue(this, 1.0f);
-    }
-
-    /**
-     * Resets task item view to empty, loading UI.
-     */
-    public void resetToEmptyUi() {
-        mIconDrawable.resetDrawable();
-        mThumbnailDrawable.resetDrawable();
-        setLabel(EMPTY_LABEL);
-    }
-
-    /**
-     * Set the label for the task item. Sets to a default label if null.
-     *
-     * @param label task label
-     */
-    public void setLabel(@Nullable String label) {
-        mLabelView.setText(getSafeLabel(label));
-        // TODO: Animation for label
-    }
-
-    /**
-     * Set the icon for the task item. Sets to a default icon if null.
-     *
-     * @param icon task icon
-     */
-    public void setIcon(@Nullable Drawable icon) {
-        // TODO: Scale the icon up based off the padding on the side
-        // The icon proper is actually smaller than the drawable and has "padding" on the side for
-        // the purpose of drawing the shadow, allowing the icon to pop up, so we need to scale the
-        // view if we want the icon to be flush with the bottom of the thumbnail.
-        mIconDrawable.setCurrentDrawable(getSafeIcon(icon));
-    }
-
-    /**
-     * Set the task thumbnail for the task. Sets to a default thumbnail if null.
-     *
-     * @param thumbnailData task thumbnail data for the task
-     */
-    public void setThumbnail(@Nullable ThumbnailData thumbnailData) {
-        mThumbnailDrawable.setCurrentDrawable(getSafeThumbnail(thumbnailData));
-    }
-
-    public View getThumbnailView() {
-        return mThumbnailView;
-    }
-
-    public View getIconView() {
-        return mIconView;
-    }
-
-    public View getLabelView() {
-        return mLabelView;
-    }
-
-    /**
-     * Start a new animation from the current task content to the specified new content. The caller
-     * is responsible for the actual animation control via the property
-     * {@link #CONTENT_TRANSITION_PROGRESS}.
-     *
-     * @param endIcon the icon to animate to
-     * @param endThumbnail the thumbnail to animate to
-     * @param endLabel the label to animate to
-     */
-    public void startContentAnimation(@Nullable Drawable endIcon,
-            @Nullable ThumbnailData endThumbnail, @Nullable String endLabel) {
-        mIconDrawable.startNewTransition(getSafeIcon(endIcon));
-        mThumbnailDrawable.startNewTransition(getSafeThumbnail(endThumbnail));
-        // TODO: Animation for label
-
-        setContentTransitionProgress(0.0f);
-    }
-
-    private void setContentTransitionProgress(float progress) {
-        mContentTransitionProgress = progress;
-        mIconDrawable.setTransitionProgress(progress);
-        mThumbnailDrawable.setTransitionProgress(progress);
-        // TODO: Animation for label
-    }
-
-    private @NonNull Drawable getSafeIcon(@Nullable Drawable icon) {
-        return (icon != null) ? icon : mDefaultIcon;
-    }
-
-    private @NonNull Drawable getSafeThumbnail(@Nullable ThumbnailData thumbnailData) {
-        if (thumbnailData == null || thumbnailData.thumbnail == null) {
-            return mDefaultThumbnail;
-        }
-        int orientation = getResources().getConfiguration().orientation;
-        return new ThumbnailDrawable(getResources(), thumbnailData,
-                orientation /* requestedOrientation */);
-    }
-
-    private @NonNull String getSafeLabel(@Nullable String label) {
-        return (label != null) ? label : DEFAULT_LABEL;
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        onOrientationChanged(getResources().getConfiguration().orientation);
-    }
-
-    @Override
-    protected void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-        onOrientationChanged(newConfig.orientation);
-    }
-
-    private void onOrientationChanged(int newOrientation) {
-        if (mDisplayedOrientation == newOrientation) {
-            return;
-        }
-        mDisplayedOrientation = newOrientation;
-        int layerCount = mThumbnailDrawable.getNumberOfLayers();
-        for (int i = 0; i < layerCount; i++) {
-            Drawable drawable = mThumbnailDrawable.getDrawable(i);
-            if (drawable instanceof ThumbnailDrawable) {
-                ((ThumbnailDrawable) drawable).setRequestedOrientation(newOrientation);
-            }
-        }
-        mTaskIconThumbnailView.forceLayout();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java b/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java
deleted file mode 100644
index 98b66b9..0000000
--- a/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.views;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.R;
-
-/**
- * A layer drawable for task content that transitions between two drawables by crossfading. Similar
- * to {@link android.graphics.drawable.TransitionDrawable} but allows callers to control transition
- * progress and provides a default, empty drawable.
- */
-public final class TaskLayerDrawable extends LayerDrawable {
-    private final Drawable mEmptyDrawable;
-    private float mProgress;
-
-    public TaskLayerDrawable(Context context) {
-        super(new Drawable[0]);
-
-        // Use empty drawable for both layers initially.
-        mEmptyDrawable = context.getResources().getDrawable(
-                R.drawable.empty_content_box, context.getTheme());
-        addLayer(mEmptyDrawable);
-        addLayer(mEmptyDrawable);
-        setTransitionProgress(1.0f);
-    }
-
-    /**
-     * Immediately set the front-most drawable layer.
-     *
-     * @param drawable drawable to set
-     */
-    public void setCurrentDrawable(@NonNull Drawable drawable) {
-        setDrawable(0, drawable);
-        applyTransitionProgress(mProgress);
-    }
-
-    /**
-     * Immediately reset the drawable to showing the empty drawable.
-     */
-    public void resetDrawable() {
-        setCurrentDrawable(mEmptyDrawable);
-    }
-
-    /**
-     * Prepare to start animating the transition by pushing the current drawable to the back and
-     * setting a new drawable to the front layer and making it invisible.
-     *
-     * @param endDrawable drawable to animate to
-     */
-    public void startNewTransition(@NonNull Drawable endDrawable) {
-        Drawable oldDrawable = getDrawable(0);
-        setDrawable(1, oldDrawable);
-        setDrawable(0, endDrawable);
-        setTransitionProgress(0.0f);
-    }
-
-    /**
-     * Set the progress of the transition animation to crossfade the two drawables.
-     *
-     * @param progress current transition progress between 0 (front view invisible) and 1
-     *                 (front view visible)
-     */
-    public void setTransitionProgress(float progress) {
-        if (progress > 1 || progress < 0) {
-            throw new IllegalArgumentException("Transition progress should be between 0 and 1");
-        }
-        mProgress = progress;
-        applyTransitionProgress(progress);
-    }
-
-    private void applyTransitionProgress(float progress) {
-        int drawableAlpha = (int) (progress * 255);
-        getDrawable(0).setAlpha(drawableAlpha);
-        if (getDrawable(0) != getDrawable(1)) {
-            // Only do this if it's a different drawable so that it fades out.
-            // Otherwise, we'd just be overwriting the front drawable's alpha.
-            getDrawable(1).setAlpha(255 - drawableAlpha);
-        }
-        invalidateSelf();
-    }
-}
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskThumbnailIconView.java b/go/quickstep/src/com/android/quickstep/views/TaskThumbnailIconView.java
deleted file mode 100644
index eaefa21..0000000
--- a/go/quickstep/src/com/android/quickstep/views/TaskThumbnailIconView.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.quickstep.views;
-
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.view.View.MeasureSpec.makeMeasureSpec;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.launcher3.R;
-
-/**
- * Square view that holds thumbnail and icon and shrinks them appropriately so that both fit nicely
- * within the view. Side length is determined by height.
- */
-final class TaskThumbnailIconView extends ViewGroup {
-    private final Rect mTmpFrameRect = new Rect();
-    private final Rect mTmpChildRect = new Rect();
-    private View mThumbnailView;
-    private View mIconView;
-    private static final float SUBITEM_FRAME_RATIO = .6f;
-
-    public TaskThumbnailIconView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mThumbnailView = findViewById(R.id.task_thumbnail);
-        mIconView = findViewById(R.id.task_icon);
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
-        int width = height;
-        setMeasuredDimension(width, height);
-
-        int subItemSize = (int) (SUBITEM_FRAME_RATIO * height);
-        if (mThumbnailView.getVisibility() != GONE) {
-            boolean isPortrait =
-                    (getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT);
-            int thumbnailHeightSpec =
-                    makeMeasureSpec(isPortrait ? height : subItemSize, MeasureSpec.EXACTLY);
-            int thumbnailWidthSpec =
-                    makeMeasureSpec(isPortrait ? subItemSize : width, MeasureSpec.EXACTLY);
-            measureChild(mThumbnailView, thumbnailWidthSpec, thumbnailHeightSpec);
-        }
-        if (mIconView.getVisibility() != GONE) {
-            int iconHeightSpec = makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
-            int iconWidthSpec = makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
-            measureChild(mIconView, iconWidthSpec, iconHeightSpec);
-        }
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        mTmpFrameRect.left = getPaddingLeft();
-        mTmpFrameRect.right = right - left - getPaddingRight();
-        mTmpFrameRect.top = getPaddingTop();
-        mTmpFrameRect.bottom = bottom - top - getPaddingBottom();
-
-        // Layout the thumbnail to the top-start corner of the view
-        if (mThumbnailView.getVisibility() != GONE) {
-            final int width = mThumbnailView.getMeasuredWidth();
-            final int height = mThumbnailView.getMeasuredHeight();
-
-            final int thumbnailGravity = Gravity.TOP | Gravity.START;
-            Gravity.apply(thumbnailGravity, width, height, mTmpFrameRect, mTmpChildRect);
-
-            mThumbnailView.layout(mTmpChildRect.left, mTmpChildRect.top,
-                    mTmpChildRect.right, mTmpChildRect.bottom);
-        }
-
-        // Layout the icon to the bottom-end corner of the view
-        if (mIconView.getVisibility() != GONE) {
-            final int width = mIconView.getMeasuredWidth();
-            final int height = mIconView.getMeasuredHeight();
-
-            int thumbnailGravity = Gravity.BOTTOM | Gravity.END;
-            Gravity.apply(thumbnailGravity, width, height, mTmpFrameRect, mTmpChildRect);
-
-            mIconView.layout(mTmpChildRect.left, mTmpChildRect.top,
-                    mTmpChildRect.right, mTmpChildRect.bottom);
-        }
-    }
-}
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/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java b/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
deleted file mode 100644
index 42b1194..0000000
--- a/go/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.shortcuts;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.ShortcutInfo;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.UserHandle;
-
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.notification.NotificationKeyData;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Performs operations related to deep shortcuts, such as querying for them, pinning them, etc.
- */
-public class DeepShortcutManager {
-
-    private static final DeepShortcutManager sInstance = new DeepShortcutManager();
-
-    public static DeepShortcutManager getInstance(Context context) {
-        return sInstance;
-    }
-
-    private final QueryResult mFailure = new QueryResult();
-
-    private DeepShortcutManager() { }
-
-    /**
-     * Queries for the shortcuts with the package name and provided ids.
-     *
-     * This method is intended to get the full details for shortcuts when they are added or updated,
-     * because we only get "key" fields in onShortcutsChanged().
-     */
-    public QueryResult queryForFullDetails(String packageName,
-            List<String> shortcutIds, UserHandle user) {
-        return mFailure;
-    }
-
-    /**
-     * Gets all the manifest and dynamic shortcuts associated with the given package and user,
-     * to be displayed in the shortcuts container on long press.
-     */
-    public QueryResult queryForShortcutsContainer(ComponentName activity,
-            UserHandle user) {
-        return mFailure;
-    }
-
-    /**
-     * Removes the given shortcut from the current list of pinned shortcuts.
-     * (Runs on background thread)
-     */
-    public void unpinShortcut(final ShortcutKey key) {
-    }
-
-    /**
-     * Adds the given shortcut to the current list of pinned shortcuts.
-     * (Runs on background thread)
-     */
-    public void pinShortcut(final ShortcutKey key) {
-    }
-
-    public void startShortcut(String packageName, String id, Rect sourceBounds,
-            Bundle startActivityOptions, UserHandle user) {
-    }
-
-    public Drawable getShortcutIconDrawable(ShortcutInfo shortcutInfo, int density) {
-        return null;
-    }
-
-    /**
-     * Returns the id's of pinned shortcuts associated with the given package and user.
-     *
-     * If packageName is null, returns all pinned shortcuts regardless of package.
-     */
-    public QueryResult queryForPinnedShortcuts(String packageName, UserHandle user) {
-        return mFailure;
-    }
-
-    public QueryResult queryForPinnedShortcuts(String packageName,
-            List<String> shortcutIds, UserHandle user) {
-        return mFailure;
-    }
-
-    public QueryResult queryForAllShortcuts(UserHandle user) {
-        return mFailure;
-    }
-
-    public boolean hasHostPermission() {
-        return false;
-    }
-
-
-    public static class QueryResult extends ArrayList<ShortcutInfo> {
-
-        public boolean wasSuccess() {
-            return true;
-        }
-    }
-}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
index 6f63d88..e807791 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
@@ -45,6 +45,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.icons.BaseIconFactory;
 import com.android.launcher3.icons.BitmapInfo;
@@ -250,9 +251,9 @@
      * @param replaceExisting if true, it will recreate the bitmap even if it already exists in
      *                        the memory. This is useful then the previous bitmap was created using
      *                        old data.
-     * package private
      */
-    protected synchronized <T> void addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic,
+    @VisibleForTesting
+    public synchronized <T> void addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic,
             PackageInfo info, long userSerial, boolean replaceExisting) {
         UserHandle user = cachingLogic.getUser(object);
         ComponentName componentName = cachingLogic.getComponent(object);
diff --git a/proguard.flags b/proguard.flags
index 272ab7a..3e12283 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -23,7 +23,10 @@
 # support jar.
 -keep class androidx.recyclerview.widget.RecyclerView { *; }
 
-# Preference fragments
+# Fragments
+-keep class ** extends androidx.fragment.app.Fragment {
+    public <init>(...);
+}
 -keep class ** extends android.app.Fragment {
     public <init>(...);
 }
@@ -50,4 +53,4 @@
 -dontwarn android.app.**
 -dontwarn android.view.**
 -dontwarn android.os.**
--dontwarn android.graphics.**
\ No newline at end of file
+-dontwarn android.graphics.**
diff --git a/protos/launcher_log.proto b/protos/launcher_log.proto
index 055ade5..0560d68 100644
--- a/protos/launcher_log.proto
+++ b/protos/launcher_log.proto
@@ -57,6 +57,7 @@
   optional TargetExtension extension = 16;
   optional TipType tip_type = 17;
   optional int32 search_query_length = 18;
+  optional bool is_work_app = 19;
 }
 
 // Used to define what type of item a Target would represent.
@@ -92,7 +93,7 @@
   TASKSWITCHER = 12; // Recents UI Container (QuickStep)
   APP = 13; // Foreground activity is another app (QuickStep)
   TIP = 14; // Onboarding texts (QuickStep)
-  SIDELOADED_LAUNCHER = 15;
+  OTHER_LAUNCHER_APP = 15;
 }
 
 // Used to define what type of control a Target would represent.
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/go/quickstep/res/drawable/empty_content_box.xml b/quickstep/recents_ui_overrides/res/drawable/hotseat_prediction_edu_top.xml
similarity index 60%
rename from go/quickstep/res/drawable/empty_content_box.xml
rename to quickstep/recents_ui_overrides/res/drawable/hotseat_prediction_edu_top.xml
index a488388..e3cc549 100644
--- a/go/quickstep/res/drawable/empty_content_box.xml
+++ b/quickstep/recents_ui_overrides/res/drawable/hotseat_prediction_edu_top.xml
@@ -1,6 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2019 The Android Open Source Project
+<!-- Copyright (C) 2020 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
@@ -14,10 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<shape
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <solid android:color="@android:color/transparent"/>
-    <stroke android:color="@android:color/white" android:width="4px"/>
-    <corners android:radius="2dp"/>
-</shape>
\ No newline at end of file
+<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/drawable/predicted_icon_background.xml b/quickstep/recents_ui_overrides/res/drawable/predicted_icon_background.xml
deleted file mode 100644
index cfc6d48..0000000
--- a/quickstep/recents_ui_overrides/res/drawable/predicted_icon_background.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
-    android:inset="@dimen/predicted_icon_background_inset">
-    <shape>
-        <solid android:color="?attr/folderFillColor" />
-        <corners android:radius="@dimen/predicted_icon_background_corner_radius" />
-    </shape>
-</inset>
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/DynamicItemCache.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
index 38bb180..76972af 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/DynamicItemCache.java
@@ -44,8 +44,8 @@
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.InstantAppResolver;
 
 import java.util.ArrayList;
@@ -167,11 +167,7 @@
 
     @WorkerThread
     private WorkspaceItemInfo loadShortcutWorker(ShortcutKey shortcutKey) {
-        DeepShortcutManager mgr = DeepShortcutManager.getInstance(mContext);
-        List<ShortcutInfo> details = mgr.queryForFullDetails(
-                shortcutKey.componentName.getPackageName(),
-                Collections.<String>singletonList(shortcutKey.getId()),
-                shortcutKey.user);
+        List<ShortcutInfo> details = shortcutKey.buildRequest(mContext).query(ShortcutRequest.ALL);
         if (!details.isEmpty()) {
             WorkspaceItemInfo si = new WorkspaceItemInfo(details.get(0), mContext);
             try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
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 72%
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 f7e71f3..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;
@@ -44,6 +58,7 @@
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.uioverrides.PredictedAppIcon;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
@@ -61,7 +76,7 @@
 public class HotseatPredictionController implements DragController.DragListener,
         View.OnAttachStateChangeListener, SystemShortcut.Factory<QuickstepLauncher>,
         InvariantDeviceProfile.OnIDPChangeListener, AllAppsStore.OnUpdateListener,
-        IconCache.ItemInfoUpdateReceiver {
+        IconCache.ItemInfoUpdateReceiver, DragSource {
 
     private static final String TAG = "PredictiveHotseat";
     private static final boolean DEBUG = false;
@@ -72,6 +87,9 @@
     private static final String APP_LOCATION_HOTSEAT = "hotseat";
     private static final String APP_LOCATION_WORKSPACE = "workspace";
 
+    private static final String BUNDLE_KEY_HOTSEAT = "hotseat_apps";
+    private static final String BUNDLE_KEY_WORKSPACE = "workspace_apps";
+
     private static final String PREDICTION_CLIENT = "hotseat";
 
     private DropTarget.DragObject mDragObject;
@@ -79,7 +97,7 @@
     private int mPredictedSpotsCount = 0;
 
     private Launcher mLauncher;
-    private Hotseat mHotseat;
+    private final Hotseat mHotseat;
 
     private List<ComponentKeyMapper> mComponentKeyMappers = new ArrayList<>();
 
@@ -87,10 +105,19 @@
 
     private AppPredictor mAppPredictor;
     private AllAppsStore mAllAppsStore;
+    private AnimatorSet mIconRemoveAnimators;
+
+    private HotseatEduController mHotseatEduController;
 
     private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>();
 
-    private static HotseatPredictionController sInstance;
+    private final View.OnLongClickListener mPredictionLongClickListener = v -> {
+        if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
+        if (mLauncher.getWorkspace().isSwitchingState()) return false;
+        // Start the drag
+        mLauncher.getWorkspace().beginDragShared(v, this, new DragOptions());
+        return false;
+    };
 
     public HotseatPredictionController(Launcher launcher) {
         mLauncher = launcher;
@@ -101,7 +128,26 @@
         mHotSeatItemsCount = mLauncher.getDeviceProfile().inv.numHotseatIcons;
         launcher.getDeviceProfile().inv.addOnChangeListener(this);
         mHotseat.addOnAttachStateChangeListener(this);
-        sInstance = this;
+        if (mHotseat.isAttachedToWindow()) {
+            onViewAttachedToWindow(mHotseat);
+        }
+    }
+
+    /**
+     * 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
@@ -119,12 +165,23 @@
     }
 
     private void fillGapsWithPrediction(boolean animate, Runnable callback) {
-        if (mDragObject != null) {
+        if (!isReady() || mDragObject != null) {
             return;
         }
         List<WorkspaceItemInfo> predictedApps = mapToWorkspaceItemInfo(mComponentKeyMappers);
         int predictionIndex = 0;
         ArrayList<WorkspaceItemInfo> newItems = new ArrayList<>();
+        // make sure predicted icon removal and filling predictions don't step on each other
+        if (mIconRemoveAnimators != null && mIconRemoveAnimators.isRunning()) {
+            mIconRemoveAnimators.addListener(new AnimationSuccessListener() {
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    fillGapsWithPrediction(animate, callback);
+                    mIconRemoveAnimators.removeListener(this);
+                }
+            });
+            return;
+        }
         for (int rank = 0; rank < mHotSeatItemsCount; rank++) {
             View child = mHotseat.getChildAt(
                     mHotseat.getCellXFromOrder(rank),
@@ -140,12 +197,11 @@
                 }
                 continue;
             }
-
             WorkspaceItemInfo predictedItem = predictedApps.get(predictionIndex++);
             if (isPredictedIcon(child) && child.isEnabled()) {
                 PredictedAppIcon icon = (PredictedAppIcon) child;
                 icon.applyFromWorkspaceItem(predictedItem);
-                icon.finishBinding();
+                icon.finishBinding(mPredictionLongClickListener);
             } else {
                 newItems.add(predictedItem);
             }
@@ -160,7 +216,7 @@
         for (WorkspaceItemInfo item : itemsToAdd) {
             PredictedAppIcon icon = PredictedAppIcon.createIcon(mHotseat, item);
             mLauncher.getWorkspace().addInScreenFromBind(icon, item);
-            icon.finishBinding();
+            icon.finishBinding(mPredictionLongClickListener);
             if (animate) {
                 animationSet.play(ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0.2f, 1));
             }
@@ -210,16 +266,23 @@
         mAppPredictor.registerPredictionUpdates(mLauncher.getMainExecutor(),
                 this::setPredictedApps);
 
+        if (!isReady()) {
+            if (mHotseatEduController != null) {
+                mHotseatEduController.destroy();
+            }
+            mHotseatEduController = new HotseatEduController(mLauncher);
+        }
         mAppPredictor.requestPredictionUpdate();
     }
 
     private Bundle getAppPredictionContextExtra() {
         Bundle bundle = new Bundle();
-        bundle.putParcelableArrayList(APP_LOCATION_HOTSEAT,
+        bundle.putParcelableArrayList(BUNDLE_KEY_HOTSEAT,
                 getPinnedAppTargetsInViewGroup((mHotseat.getShortcutsAndWidgets())));
-        bundle.putParcelableArrayList(APP_LOCATION_WORKSPACE, getPinnedAppTargetsInViewGroup(
+        bundle.putParcelableArrayList(BUNDLE_KEY_WORKSPACE, getPinnedAppTargetsInViewGroup(
                 mLauncher.getWorkspace().getScreenWithId(
                         Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets()));
+
         return bundle;
     }
 
@@ -248,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() {
@@ -285,9 +352,12 @@
             ItemInfoWithIcon info = mapper.getApp(allAppsStore);
             if (info instanceof AppInfo) {
                 WorkspaceItemInfo predictedApp = new WorkspaceItemInfo((AppInfo) info);
+                predictedApp.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
                 predictedApps.add(predictedApp);
             } else if (info instanceof WorkspaceItemInfo) {
-                predictedApps.add(new WorkspaceItemInfo((WorkspaceItemInfo) info));
+                WorkspaceItemInfo predictedApp = new WorkspaceItemInfo((WorkspaceItemInfo) info);
+                predictedApp.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
+                predictedApps.add(predictedApp);
             } else {
                 if (DEBUG) {
                     Log.e(TAG, "Predicted app not found: " + mapper);
@@ -313,13 +383,27 @@
         return icons;
     }
 
-    private void removePredictedApps(List<PredictedAppIcon.PredictedIconOutlineDrawing> outlines) {
+    private void removePredictedApps(List<PredictedAppIcon.PredictedIconOutlineDrawing> outlines,
+            ItemInfo draggedInfo) {
+        if (mIconRemoveAnimators != null) {
+            mIconRemoveAnimators.end();
+        }
+        mIconRemoveAnimators = new AnimatorSet();
+        removeOutlineDrawings();
         for (PredictedAppIcon icon : getPredictedIcons()) {
+            if (!icon.isEnabled()) {
+                continue;
+            }
+            if (icon.getTag().equals(draggedInfo)) {
+                mHotseat.removeView(icon);
+                continue;
+            }
             int rank = ((WorkspaceItemInfo) icon.getTag()).rank;
             outlines.add(new PredictedAppIcon.PredictedIconOutlineDrawing(
                     mHotseat.getCellXFromOrder(rank), mHotseat.getCellYFromOrder(rank), icon));
             icon.setEnabled(false);
-            icon.animate().scaleY(0).scaleX(0).setListener(new AnimationSuccessListener() {
+            ObjectAnimator animator = ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0);
+            animator.addListener(new AnimationSuccessListener() {
                 @Override
                 public void onAnimationSuccess(Animator animator) {
                     if (icon.getParent() != null) {
@@ -327,10 +411,11 @@
                     }
                 }
             });
+            mIconRemoveAnimators.play(animator);
         }
+        mIconRemoveAnimators.start();
     }
 
-
     private void notifyItemAction(AppTarget target, String location, int action) {
         if (mAppPredictor != null) {
             mAppPredictor.notifyAppTargetEvent(new AppTargetEvent.Builder(target,
@@ -340,7 +425,7 @@
 
     @Override
     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
-        removePredictedApps(mOutlineDrawings);
+        removePredictedApps(mOutlineDrawings, dragObject.dragInfo);
         mDragObject = dragObject;
         if (mOutlineDrawings.isEmpty()) return;
         for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
@@ -354,14 +439,25 @@
         if (mDragObject == null) {
             return;
         }
+
         ItemInfo dragInfo = mDragObject.dragInfo;
-        if (dragInfo instanceof WorkspaceItemInfo && dragInfo.getTargetComponent() != null) {
+        ViewGroup hotseatVG = mHotseat.getShortcutsAndWidgets();
+        ViewGroup firstScreenVG = mLauncher.getWorkspace().getScreenWithId(
+                Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets();
+
+        if (dragInfo instanceof WorkspaceItemInfo
+                && dragInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+                && dragInfo.getTargetComponent() != null) {
             AppTarget appTarget = getAppTargetFromItemInfo(dragInfo);
             if (!isInHotseat(dragInfo) && isInHotseat(mDragObject.originalDragInfo)) {
-                notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, APPTARGET_ACTION_UNPIN);
+                if (!getPinnedAppTargetsInViewGroup(hotseatVG).contains(appTarget)) {
+                    notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, APPTARGET_ACTION_UNPIN);
+                }
             }
             if (!isInFirstPage(dragInfo) && isInFirstPage(mDragObject.originalDragInfo)) {
-                notifyItemAction(appTarget, APP_LOCATION_WORKSPACE, APPTARGET_ACTION_UNPIN);
+                if (!getPinnedAppTargetsInViewGroup(firstScreenVG).contains(appTarget)) {
+                    notifyItemAction(appTarget, APP_LOCATION_WORKSPACE, APPTARGET_ACTION_UNPIN);
+                }
             }
             if (isInHotseat(dragInfo) && !isInHotseat(mDragObject.originalDragInfo)) {
                 notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, AppTargetEvent.ACTION_PIN);
@@ -371,14 +467,7 @@
             }
         }
         mDragObject = null;
-        fillGapsWithPrediction(true, () -> {
-            if (mOutlineDrawings.isEmpty()) return;
-            for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
-                mHotseat.removeDelegatedCellDrawing(outlineDrawing);
-            }
-            mHotseat.invalidate();
-            mOutlineDrawings.clear();
-        });
+        fillGapsWithPrediction(true, this::removeOutlineDrawings);
     }
 
     @Nullable
@@ -394,11 +483,20 @@
     private void preparePredictionInfo(WorkspaceItemInfo itemInfo, int rank) {
         itemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
         itemInfo.rank = rank;
-        itemInfo.cellX = rank;
-        itemInfo.cellY = mHotSeatItemsCount - rank - 1;
+        itemInfo.cellX = mHotseat.getCellXFromOrder(rank);
+        itemInfo.cellY = mHotseat.getCellYFromOrder(rank);
         itemInfo.screenId = rank;
     }
 
+    private void removeOutlineDrawings() {
+        if (mOutlineDrawings.isEmpty()) return;
+        for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
+            mHotseat.removeDelegatedCellDrawing(outlineDrawing);
+        }
+        mHotseat.invalidate();
+        mOutlineDrawings.clear();
+    }
+
     @Override
     public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
         this.mHotSeatItemsCount = profile.numHotseatIcons;
@@ -411,8 +509,17 @@
     }
 
     @Override
-    public void reapplyItemInfo(ItemInfoWithIcon info) {
+    public void reapplyItemInfo(ItemInfoWithIcon info) {}
 
+    @Override
+    public void onDropCompleted(View target, DropTarget.DragObject d, boolean success) {
+        //Does nothing
+    }
+
+    @Override
+    public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
+            LauncherLogProto.Target targetParent) {
+        mHotseat.fillInLogContainerData(v, info, target, targetParent);
     }
 
     private class PinPrediction extends SystemShortcut<QuickstepLauncher> {
@@ -435,18 +542,22 @@
      */
     public static void fillInHybridHotseatRank(
             @NonNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target) {
-        if (sInstance == null || itemInfo.getTargetComponent() == null
+        QuickstepLauncher launcher = QuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity();
+        if (launcher == null || launcher.getHotseatPredictionController() == null
+                || itemInfo.getTargetComponent() == null
                 || itemInfo.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
             return;
         }
+        HotseatPredictionController controller = launcher.getHotseatPredictionController();
+
         final ComponentKey k = new ComponentKey(itemInfo.getTargetComponent(), itemInfo.user);
 
-        final List<ComponentKeyMapper> predictedApps = sInstance.mComponentKeyMappers;
+        final List<ComponentKeyMapper> predictedApps = controller.mComponentKeyMappers;
         IntStream.range(0, predictedApps.size())
                 .filter((i) -> k.equals(predictedApps.get(i).getComponentKey()))
                 .findFirst()
                 .ifPresent((rank) -> target.predictedRank =
-                        Integer.parseInt(sInstance.mPredictedSpotsCount + "0" + rank));
+                        Integer.parseInt(controller.mPredictedSpotsCount + "0" + rank));
     }
 
     private static boolean isPredictedIcon(View view) {
@@ -461,8 +572,7 @@
         }
         ItemInfo info = (ItemInfo) view.getTag();
         return info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION && (
-                info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
-                        || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT);
+                info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION);
     }
 
     private static boolean isInHotseat(ItemInfo itemInfo) {
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 1dcbffb..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
@@ -29,7 +29,6 @@
 
 import androidx.core.graphics.ColorUtils;
 
-import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
@@ -37,7 +36,6 @@
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.icons.IconNormalizer;
-import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.views.DoubleShadowBubbleTextView;
@@ -47,14 +45,13 @@
  */
 public class PredictedAppIcon extends DoubleShadowBubbleTextView {
 
-    private static final float RING_EFFECT_RATIO = 0.12f;
+    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;
 
-
     public PredictedAppIcon(Context context) {
         this(context, null, 0);
     }
@@ -68,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
@@ -105,14 +102,8 @@
     /**
      * prepares prediction icon for usage after bind
      */
-    public void finishBinding() {
-        setOnLongClickListener((v) -> {
-            PopupContainerWithArrow.showForIcon((BubbleTextView) v);
-            if (getParent() != null) {
-                getParent().requestDisallowInterceptTouchEvent(true);
-            }
-            return true;
-        });
+    public void finishBinding(OnLongClickListener longClickListener) {
+        setOnLongClickListener(longClickListener);
         ((CellLayout.LayoutParams) getLayoutParams()).canReorder = false;
         setTextVisibility(false);
         verifyHighRes();
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 37a3929..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;
@@ -178,6 +178,13 @@
     }
 
     /**
+     * Returns Prediction controller for hybrid hotseat
+     */
+    public HotseatPredictionController getHotseatPredictionController() {
+        return mHotseatPredictionController;
+    }
+
+    /**
      * Recents logic that triggers when launcher state changes or launcher activity stops/resumes.
      */
     private void onStateOrResumeChanged() {
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/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
index 630dd70..7ff8969 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
@@ -75,9 +76,11 @@
     protected static final Rect TEMP_RECT = new Rect();
 
     // Start resisting when swiping past this factor of mTransitionDragLength.
-    private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = 1.4f;
+    private static final float DRAG_LENGTH_FACTOR_START_PULLBACK = ENABLE_OVERVIEW_ACTIONS.get()
+            ? 2.8f : 1.4f;
     // This is how far down we can scale down, where 0f is full screen and 1f is recents.
-    private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = 1.8f;
+    private static final float DRAG_LENGTH_FACTOR_MAX_PULLBACK = ENABLE_OVERVIEW_ACTIONS.get()
+            ? 3.6f : 1.8f;
     private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
 
     // The distance needed to drag to reach the task size in recents.
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
index f889bc1..6574d22 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityInterface.java
@@ -226,7 +226,7 @@
         RecentsActivity activity = getCreatedActivity();
         boolean visible = activity != null && activity.isStarted() && activity.hasWindowFocus();
         return visible
-                ? LauncherLogProto.ContainerType.SIDELOADED_LAUNCHER
+                ? LauncherLogProto.ContainerType.OTHER_LAUNCHER_APP
                 : LauncherLogProto.ContainerType.APP;
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
index 888ea9c..700feef 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -453,8 +453,10 @@
 
     @Override
     public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
-        super.onRecentsAnimationCanceled(thumbnailData);
         mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
+
+        // Defer clearing the controller and the targets until after we've updated the state
+        super.onRecentsAnimationCanceled(thumbnailData);
     }
 
     /**
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 8f75c79..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;
@@ -610,10 +610,12 @@
 
     @Override
     public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
-        super.onRecentsAnimationCanceled(thumbnailData);
+        ActiveGestureLog.INSTANCE.addLog("cancelRecentsAnimation");
         mActivityInitListener.unregister();
         mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
-        ActiveGestureLog.INSTANCE.addLog("cancelRecentsAnimation");
+
+        // Defer clearing the controller and the targets until after we've updated the state
+        super.onRecentsAnimationCanceled(thumbnailData);
     }
 
     @Override
@@ -971,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/OverviewActionsFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewActionsFactory.java
deleted file mode 100644
index 6d17b27..0000000
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewActionsFactory.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep;
-
-import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
-
-import android.view.View;
-
-import com.android.launcher3.R;
-import com.android.launcher3.util.MainThreadInitializedObject;
-import com.android.launcher3.util.ResourceBasedOverride;
-
-/**
- * Overview actions are shown in overview underneath the task snapshot. This factory class is
- * overrideable in an overlay. The {@link OverviewActions} class provides the view that should be
- * shown in the Overview.
- */
-public class OverviewActionsFactory implements ResourceBasedOverride {
-
-    public static final MainThreadInitializedObject<OverviewActionsFactory> INSTANCE =
-            forOverride(OverviewActionsFactory.class, R.string.overview_actions_factory_class);
-
-    /** Create a new Overview Actions for interacting between the actions and overview. */
-    public OverviewActions createOverviewActions() {
-        return new OverviewActions();
-    }
-
-    /** Overlay overrideable, base class does nothing. */
-    public static class OverviewActions {
-        /** Get the view to show in the overview. */
-        public View getView() {
-            return null;
-        }
-    }
-}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
index b5441df..ec7cddf 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TaskOverlayFactory.java
@@ -19,6 +19,9 @@
 import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
 
 import android.graphics.Matrix;
+import android.view.View;
+
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.BaseDraggingActivity;
@@ -75,6 +78,11 @@
          */
         public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix) { }
 
+        @Nullable
+        public View getActionsView() {
+            return null;
+        }
+
         /**
          * Called when the overlay is no longer used.
          */
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 bafb2ef..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;
@@ -484,7 +485,9 @@
                 base = new AssistantInputConsumer(this, newGestureState, base, mInputMonitorCompat);
             }
 
-            if (mOverscrollPlugin != null) {
+            if (FeatureFlags.ENABLE_QUICK_CAPTURE_GESTURE.get()
+                    && (mOverscrollPlugin != null)
+                    && mOverscrollPlugin.isActive()) {
                 // Put the overscroll gesture as higher priority than the Assistant or base gestures
                 base = new OverscrollInputConsumer(this, newGestureState, base, mInputMonitorCompat,
                         mOverscrollPlugin);
@@ -713,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/inputconsumers/OverscrollInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
index e3da98b..0a21413 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
@@ -26,14 +26,17 @@
 
 import android.content.Context;
 import android.graphics.PointF;
+import android.view.GestureDetector;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.R;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
+import com.android.quickstep.views.LauncherRecentsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.systemui.plugins.OverscrollPlugin;
 import com.android.systemui.shared.system.InputMonitorCompat;
@@ -47,12 +50,12 @@
 
     private static final String TAG = "OverscrollInputConsumer";
 
-    private static final int ANGLE_THRESHOLD = 35; // Degrees
-
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
     private final PointF mStartDragPos = new PointF();
+    private final int mAngleThreshold;
 
+    private final float mFlingThresholdPx;
     private int mActivePointerId = -1;
     private boolean mPassedSlop = false;
 
@@ -60,19 +63,28 @@
 
     private final Context mContext;
     private final GestureState mGestureState;
-    @Nullable private final OverscrollPlugin mPlugin;
+    @Nullable
+    private final OverscrollPlugin mPlugin;
+    private final GestureDetector mGestureDetector;
 
     private RecentsView mRecentsView;
 
     public OverscrollInputConsumer(Context context, GestureState gestureState,
             InputConsumer delegate, InputMonitorCompat inputMonitor, OverscrollPlugin plugin) {
         super(delegate, inputMonitor);
+
+        mAngleThreshold = context.getResources()
+                .getInteger(R.integer.assistant_gesture_corner_deg_threshold);
+        mFlingThresholdPx = context.getResources()
+            .getDimension(R.dimen.gestures_overscroll_fling_threshold);
         mContext = context;
         mGestureState = gestureState;
         mPlugin = plugin;
 
         float slop = ViewConfiguration.get(context).getScaledTouchSlop();
+
         mSquaredSlop = slop * slop;
+        mGestureDetector = new GestureDetector(context, new FlingGestureListener());
 
         gestureState.getActivityInterface().createActivityInitListener(this::onActivityInit)
                 .register();
@@ -139,21 +151,29 @@
 
                         mPassedSlop = true;
                         mStartDragPos.set(mLastPos.x, mLastPos.y);
-
                         if (isOverscrolled()) {
                             setActive(ev);
+
+                            if (mPlugin != null) {
+                                mPlugin.onTouchStart(getDeviceState(), getUnderlyingActivity());
+                            }
                         } else {
                             mState = STATE_DELEGATE_ACTIVE;
                         }
                     }
                 }
 
+                if (mPassedSlop && mState != STATE_DELEGATE_ACTIVE && isOverscrolled()
+                        && mPlugin != null) {
+                    mPlugin.onTouchTraveled(getDistancePx());
+                }
+
                 break;
             }
             case ACTION_CANCEL:
             case ACTION_UP:
                 if (mState != STATE_DELEGATE_ACTIVE && mPassedSlop && mPlugin != null) {
-                    mPlugin.onOverscroll(getDeviceState());
+                    mPlugin.onTouchEnd(getDistancePx());
                 }
 
                 mPassedSlop = false;
@@ -161,6 +181,10 @@
                 break;
         }
 
+        if (mState != STATE_DELEGATE_ACTIVE) {
+            mGestureDetector.onTouchEvent(ev);
+        }
+
         if (mState != STATE_ACTIVE) {
             mDelegate.onMotionEvent(ev);
         }
@@ -168,12 +192,19 @@
 
     private boolean isOverscrolled() {
         // Make sure there isn't an app to quick switch to on our right
-        boolean atRightMostApp = (mRecentsView == null || mRecentsView.getRunningTaskIndex() <= 0);
+        int maxIndex = 0;
+        if ((mRecentsView instanceof LauncherRecentsView)
+                && ((LauncherRecentsView) mRecentsView).hasRecentsExtraCard()) {
+            maxIndex = 1;
+        }
+
+        boolean atRightMostApp = (mRecentsView == null
+                || mRecentsView.getRunningTaskIndex() <= maxIndex);
 
         // Check if the gesture is within our angle threshold of horizontal
         float deltaY = Math.abs(mLastPos.y - mDownPos.y);
         float deltaX = mDownPos.x - mLastPos.x; // Positive if this is a gesture to the left
-        boolean angleInBounds = Math.toDegrees(Math.atan2(deltaY, deltaX)) < ANGLE_THRESHOLD;
+        boolean angleInBounds = Math.toDegrees(Math.atan2(deltaY, deltaX)) < mAngleThreshold;
 
         return atRightMostApp && angleInBounds;
     }
@@ -193,4 +224,36 @@
 
         return deviceState;
     }
+
+    private int getDistancePx() {
+        return (int) Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y);
+    }
+
+    private String getUnderlyingActivity() {
+        return mGestureState.getRunningTask().topActivity.flattenToString();
+    }
+
+    private class FlingGestureListener extends GestureDetector.SimpleOnGestureListener {
+        @Override
+        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+            if (isValidAngle(velocityX, -velocityY)
+                    && getDistancePx() >= mFlingThresholdPx
+                    && mState != STATE_DELEGATE_ACTIVE) {
+
+                if (mPlugin != null) {
+                    mPlugin.onFling(-velocityX);
+                }
+            }
+            return true;
+        }
+
+        private boolean isValidAngle(float deltaX, float deltaY) {
+            float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
+            // normalize so that angle is measured clockwise from horizontal in the bottom right
+            // corner and counterclockwise from horizontal in the bottom left corner
+
+            angle = angle > 90 ? 180 - angle : angle;
+            return (angle < mAngleThreshold);
+        }
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
index 82fbbc6..1bbb3f5 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -378,6 +378,11 @@
     }
 
     @Override
+    public boolean hasRecentsExtraCard() {
+        return mRecentsExtraViewContainer != null;
+    }
+
+    @Override
     public void setContentAlpha(float alpha) {
         super.setContentAlpha(alpha);
         if (mRecentsExtraViewContainer != null) {
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 0f0fda9..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;
@@ -830,6 +830,11 @@
 
     public abstract void startHome();
 
+    /** `true` if there is a +1 space available in overview. */
+    public boolean hasRecentsExtraCard() {
+        return false;
+    }
+
     public void reset() {
         setCurrentTask(-1);
         mIgnoreResetTaskId = -1;
@@ -1100,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);
         }
@@ -1180,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,
@@ -1310,7 +1315,7 @@
     }
 
     private void dismissCurrentTask() {
-        TaskView taskView = getTaskView(getNextPage());
+        TaskView taskView = getNextPageTaskView();
         if (taskView != null) {
             dismissTask(taskView, true /*animateTaskView*/, true /*removeTask*/);
         }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
index 4e0fdea..3bc1509 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -197,6 +197,10 @@
         updateThumbnailPaintFilter();
     }
 
+    public TaskOverlay getTaskOverlay() {
+        return mOverlay;
+    }
+
     public float getDimAlpha() {
         return mDimAlpha;
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
index 94cec72..6e1b24a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
@@ -62,7 +62,6 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.PendingAnimation;
 import com.android.launcher3.util.ViewPool.Reusable;
-import com.android.quickstep.OverviewActionsFactory;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TaskIconCache;
 import com.android.quickstep.TaskOverlayFactory;
@@ -164,7 +163,6 @@
     private final float mWindowCornerRadius;
     private final BaseDraggingActivity mActivity;
 
-    private OverviewActionsFactory.OverviewActions mOverviewActions;
     @Nullable private View mActionsView;
 
     private ObjectAnimator mIconAndDimAnimator;
@@ -222,7 +220,6 @@
         mCurrentFullscreenParams = new FullscreenDrawParams(mCornerRadius);
         mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this);
 
-        mOverviewActions = OverviewActionsFactory.INSTANCE.get(context).createOverviewActions();
         mOutlineProvider = new TaskOutlineProvider(getResources(), mCurrentFullscreenParams);
         setOutlineProvider(mOutlineProvider);
     }
@@ -239,7 +236,7 @@
 
 
         if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
-            mActionsView = mOverviewActions.getView();
+            mActionsView = mSnapshotView.getTaskOverlay().getActionsView();
             if (mActionsView != null) {
                 TaskView.LayoutParams params = new TaskView.LayoutParams(LayoutParams.MATCH_PARENT,
                         getResources().getDimensionPixelSize(R.dimen.overview_actions_height),
diff --git a/quickstep/res/drawable-v28/back_gesture_tutorial_action_button_background.xml b/quickstep/res/drawable-v28/back_gesture_tutorial_action_button_background.xml
new file mode 100644
index 0000000..cd30ef7
--- /dev/null
+++ b/quickstep/res/drawable-v28/back_gesture_tutorial_action_button_background.xml
@@ -0,0 +1,20 @@
+<!--
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners android:radius="?android:attr/dialogCornerRadius"/>
+    <solid android:color="@color/back_gesture_tutorial_primary_color"/>
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/back_gesture.xml b/quickstep/res/drawable/back_gesture.xml
new file mode 100644
index 0000000..a5c57b4
--- /dev/null
+++ b/quickstep/res/drawable/back_gesture.xml
@@ -0,0 +1,367 @@
+<!--
+    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.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector
+            android:width="206dp"
+            android:height="435dp"
+            android:viewportWidth="206"
+            android:viewportHeight="435">
+            <group android:name="edgeGroup"
+                android:translateX="197"
+                android:translateY="0">
+                <path
+                    android:name="edge"
+                    android:fillAlpha="0"
+                    android:fillType="nonZero"
+                    android:fillColor="#1a73eb"
+                    android:pathData=" M0,0 h9 v435 h-9 z " />
+            </group>
+            <group
+                android:name="trailGroup"
+                android:translateX="226"
+                android:translateY="200">
+                <path
+                    android:name="trail"
+                    android:fillAlpha="1"
+                    android:fillType="nonZero"
+                    android:pathData=" M0,0 h55 v36 h-55 z ">
+                    <aapt:attr name="android:fillColor">
+                        <gradient
+                            android:startX="0"
+                            android:endX="55"
+                            android:type="linear">
+                            <item
+                                android:color="#991a73eb"
+                                android:offset="0" />
+                            <item
+                                android:color="#401a73eb"
+                                android:offset="0.5" />
+                            <item
+                                android:color="#001a73eb"
+                                android:offset="1" />
+                        </gradient>
+                    </aapt:attr>
+                </path>
+            </group>
+            <group android:name="_R_G">
+                <group
+                    android:name="_R_G_L_0_G_T_1"
+                    android:rotation="11"
+                    android:scaleX="0.9"
+                    android:scaleY="0.9"
+                    android:translateX="309"
+                    android:translateY="422.5">
+                    <group
+                        android:name="_R_G_L_0_G"
+                        android:translateX="-145"
+                        android:translateY="-208">
+                        <path
+                            android:name="_R_G_L_0_G_D_0_P_0"
+                            android:fillAlpha="1"
+                            android:fillColor="#d2e3fc"
+                            android:fillType="nonZero"
+                            android:pathData=" M12.5 -47 C-7.93,-41.24 -3,-20.5 -1.5,-7 C0,6.5 2.5,22 9,39.5 C13.52,51.67 17.06,63.52 19,113 C21,164 53.5,243.5 53.5,243.5 C53.5,243.5 59,275.5 123.5,326 C188,376.5 283.5,236 290.5,199 C297.5,162 194.5,80 149,73 C103.5,66 90.5,57.5 77,50 C63.5,42.5 57,27 54.5,13.5 C52,0 43.5,-15 40,-25 C36.5,-35 32,-52.5 12.5,-47c " />
+                        <path
+                            android:name="_R_G_L_0_G_D_1_P_0"
+                            android:pathData=" M4.45 -34.66 C4.45,-34.66 10.5,-12.66 10.5,-12.66 C11.24,-9.98 13.98,-8.38 16.67,-9.04 C16.67,-9.04 29.72,-12.27 29.72,-12.27 C32.39,-12.93 34.05,-15.59 33.47,-18.28 C33.47,-18.28 32.11,-24.57 32.11,-24.57 "
+                            android:strokeWidth="4"
+                            android:strokeAlpha="1"
+                            android:strokeColor="#a0c2f9" />
+                        <path
+                            android:name="_R_G_L_0_G_D_2_P_0"
+                            android:pathData=" M18.35 21.81 C21.41,17.24 36.97,10.77 44.63,13.55 "
+                            android:strokeWidth="4"
+                            android:strokeAlpha="1"
+                            android:strokeColor="#a0c2f9" />
+                    </group>
+                </group>
+            </group>
+            <group android:name="time_group" />
+        </vector>
+    </aapt:attr>
+    <target android:name="edge">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="333"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="0"
+                    android:valueFrom="0"
+                    android:valueTo="0.2"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="917"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="333"
+                    android:valueFrom="0.2"
+                    android:valueTo="0.2"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="583"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="1250"
+                    android:valueFrom="0.2"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="trail">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="2000"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="0"
+                    android:valueFrom="1"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="850"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="2000"
+                    android:valueFrom="1"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="trailGroup">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="83"
+                    android:propertyName="translateX"
+                    android:startOffset="1250"
+                    android:valueFrom="226"
+                    android:valueTo="226"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.285,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1000"
+                    android:propertyName="translateX"
+                    android:startOffset="1333"
+                    android:valueFrom="226"
+                    android:valueTo="151"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.285,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="517"
+                    android:propertyName="translateX"
+                    android:startOffset="2333"
+                    android:valueFrom="151"
+                    android:valueTo="151"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.285,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="50"
+                    android:propertyName="translateX"
+                    android:startOffset="2850"
+                    android:valueFrom="226"
+                    android:valueTo="226"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.285,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="1833"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="1250"
+                    android:valueFrom="1"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="167"
+                    android:propertyName="fillAlpha"
+                    android:startOffset="3083"
+                    android:valueFrom="1"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_1_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="1833"
+                    android:propertyName="strokeAlpha"
+                    android:startOffset="1250"
+                    android:valueFrom="1"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="100"
+                    android:propertyName="strokeAlpha"
+                    android:startOffset="3083"
+                    android:valueFrom="1"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_D_2_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="1833"
+                    android:propertyName="strokeAlpha"
+                    android:startOffset="1250"
+                    android:valueFrom="1"
+                    android:valueTo="1"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="100"
+                    android:propertyName="strokeAlpha"
+                    android:startOffset="3083"
+                    android:valueFrom="1"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="83"
+                    android:propertyName="translateX"
+                    android:startOffset="1250"
+                    android:valueFrom="309"
+                    android:valueTo="309"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.285,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1417"
+                    android:propertyName="translateX"
+                    android:startOffset="1333"
+                    android:valueFrom="309"
+                    android:valueTo="251"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.285,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="_R_G_L_0_G_T_1">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="83"
+                    android:propertyName="rotation"
+                    android:startOffset="1250"
+                    android:valueFrom="11"
+                    android:valueTo="11"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.277,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+                <objectAnimator
+                    android:duration="1417"
+                    android:propertyName="rotation"
+                    android:startOffset="1333"
+                    android:valueFrom="11"
+                    android:valueTo="0"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.277,1 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator
+                    android:duration="2183"
+                    android:propertyName="translateX"
+                    android:startOffset="1250"
+                    android:valueFrom="0"
+                    android:valueTo="1"
+                    android:valueType="floatType" />
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/back_gesture_tutorial_action_button_background.xml b/quickstep/res/drawable/back_gesture_tutorial_action_button_background.xml
new file mode 100644
index 0000000..d7b9102
--- /dev/null
+++ b/quickstep/res/drawable/back_gesture_tutorial_action_button_background.xml
@@ -0,0 +1,20 @@
+<!--
+    Copyright (C) 2020 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners android:radius="@dimen/default_dialog_corner_radius"/>
+    <solid android:color="@color/back_gesture_tutorial_primary_color"/>
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/drawable/back_gesture_tutorial_close_button.xml b/quickstep/res/drawable/back_gesture_tutorial_close_button.xml
new file mode 100644
index 0000000..0702042
--- /dev/null
+++ b/quickstep/res/drawable/back_gesture_tutorial_close_button.xml
@@ -0,0 +1,25 @@
+<!--
+    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:height="13dp"
+    android:viewportHeight="14"
+    android:viewportWidth="14"
+    android:width="13dp">
+    <path
+        android:fillColor="#000000"
+        android:fillType="evenOdd"
+        android:pathData="M14,1.41L12.59,0L7,5.59L1.41,0L0,1.41L5.59,7L0,12.59L1.41,14L7,8.41L12.59,14L14,12.59L8.41,7L14,1.41Z"/>
+</vector>
\ No newline at end of file
diff --git a/quickstep/res/layout/back_gesture_tutorial_activity.xml b/quickstep/res/layout/back_gesture_tutorial_activity.xml
new file mode 100644
index 0000000..e894e89
--- /dev/null
+++ b/quickstep/res/layout/back_gesture_tutorial_activity.xml
@@ -0,0 +1,19 @@
+<!--
+    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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/back_gesture_tutorial_fragment_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
\ No newline at end of file
diff --git a/quickstep/res/layout/back_gesture_tutorial_fragment.xml b/quickstep/res/layout/back_gesture_tutorial_fragment.xml
new file mode 100644
index 0000000..294e46e
--- /dev/null
+++ b/quickstep/res/layout/back_gesture_tutorial_fragment.xml
@@ -0,0 +1,121 @@
+<!--
+    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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layerType="software"
+    android:background="@color/back_gesture_tutorial_background_color">
+    <!--The layout is rendered on the software layer to avoid b/136158117-->
+
+    <ImageView
+        android:id="@+id/back_gesture_tutorial_fragment_hand_coaching"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scaleType="centerCrop"/>
+
+    <ImageButton
+        android:id="@+id/back_gesture_tutorial_fragment_close_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="18dp"
+        android:layout_marginTop="30dp"
+        android:layout_marginStart="4dp"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentTop="true"
+        android:background="@android:color/transparent"
+        android:accessibilityTraversalAfter="@id/back_gesture_tutorial_fragment_titles_container"
+        android:contentDescription="@string/back_gesture_tutorial_close_button_content_description"
+        android:src="@drawable/back_gesture_tutorial_close_button"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginTop="70dp"
+        android:orientation="vertical">
+
+        <LinearLayout
+            android:id="@+id/back_gesture_tutorial_fragment_titles_container"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:focusable="true">
+
+            <TextView
+                android:id="@+id/back_gesture_tutorial_fragment_title_view"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:layout_marginStart="@dimen/back_gesture_tutorial_title_margin_start_end"
+                android:layout_marginEnd="@dimen/back_gesture_tutorial_title_margin_start_end"
+                style="@style/TextAppearance.BackGestureTutorial.Title"/>
+
+            <TextView
+                android:id="@+id/back_gesture_tutorial_fragment_subtitle_view"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:layout_marginTop="10dp"
+                android:layout_marginStart="@dimen/back_gesture_tutorial_subtitle_margin_start_end"
+                android:layout_marginEnd="@dimen/back_gesture_tutorial_subtitle_margin_start_end"
+                style="@style/TextAppearance.BackGestureTutorial.Subtitle"/>
+
+        </LinearLayout>
+
+        <Space
+            android:layout_width="wrap_content"
+            android:layout_weight="1"
+            android:layout_height="0dp"
+            android:layout_marginTop="48dp"
+            android:layout_gravity="center_horizontal"
+            android:gravity="center_horizontal"
+            android:orientation="vertical"/>
+
+        <!-- android:stateListAnimator="@null" removes shadow and normal on click behavior (increase
+             of elevation and shadow) which is replaced by ripple effect in android:foreground -->
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="46dp"
+            android:layout_marginBottom="48dp"
+            android:layout_gravity="center_horizontal">
+
+            <Button
+                android:id="@+id/back_gesture_tutorial_fragment_action_button"
+                android:layout_width="142dp"
+                android:layout_height="49dp"
+                android:layout_marginEnd="@dimen/back_gesture_tutorial_button_margin_start_end"
+                android:layout_alignParentEnd="true"
+                android:stateListAnimator="@null"
+                android:background="@drawable/back_gesture_tutorial_action_button_background"
+                android:foreground="?android:attr/selectableItemBackgroundBorderless"
+                style="@style/TextAppearance.BackGestureTutorial.ButtonLabel"/>
+
+            <Button
+                android:id="@+id/back_gesture_tutorial_fragment_action_text_button"
+                android:layout_width="142dp"
+                android:layout_height="49dp"
+                android:layout_marginStart="@dimen/back_gesture_tutorial_button_margin_start_end"
+                android:layout_alignParentStart="true"
+                android:stateListAnimator="@null"
+                android:background="@null"
+                android:foreground="?android:attr/selectableItemBackgroundBorderless"
+                style="@style/TextAppearance.BackGestureTutorial.TextButtonLabel"/>
+
+        </RelativeLayout>
+
+    </LinearLayout>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 24ab487..327bb14 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -15,8 +15,6 @@
 -->
 <resources>
     <string name="task_overlay_factory_class" translatable="false"></string>
-    <!-- Class name for factory object that creates the overview actions UI when enabled. -->
-    <string name="overview_actions_factory_class" translatable="false" />
 
     <!-- Activity which blocks home gesture -->
     <string name="gesture_blocking_activity" translatable="false"></string>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 9ff1350..988c78d 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -77,4 +77,12 @@
 
     <!-- Distance to move elements when swiping up to go home from launcher -->
     <dimen name="home_pullback_distance">28dp</dimen>
+
+    <!-- Overscroll Gesture -->
+    <dimen name="gestures_overscroll_fling_threshold">40dp</dimen>
+
+    <!-- Tips Gesture Tutorial -->
+    <dimen name="back_gesture_tutorial_title_margin_start_end">40dp</dimen>
+    <dimen name="back_gesture_tutorial_subtitle_margin_start_end">16dp</dimen>
+    <dimen name="back_gesture_tutorial_button_margin_start_end">18dp</dimen>
 </resources>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 4319b5d..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>
 
@@ -66,5 +63,49 @@
     <!-- Text of the tip when user lands in all apps view for the first time, indicating where the tip toast points to is the predicted apps section. [CHAR_LIMIT=50] -->
     <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>
 
+    <!-- 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>
+    <!-- Subtitle shown during interactive parts of Back gesture tutorial for right edge. [CHAR LIMIT=60] -->
+    <string name="back_gesture_tutorial_engaged_subtitle_swipe_inward_right_edge" translatable="false">Start at the right edge and swipe toward the middle</string>
+
+    <!-- Title shown during interactive part of Back gesture tutorial for left edge. [CHAR LIMIT=30] -->
+    <string name="back_gesture_tutorial_playground_title_swipe_inward_left_edge" translatable="false">Try the other side</string>
+    <!-- Subtitle shown during interactive parts of Back gesture tutorial for left edge. [CHAR LIMIT=60] -->
+    <string name="back_gesture_tutorial_engaged_subtitle_swipe_inward_left_edge" translatable="false">That\'s it! Now try swiping from the left edge.</string>
+
+    <!-- Title shown on the confirmation screen after successful gesture. [CHAR LIMIT=30] -->
+    <string name="back_gesture_tutorial_confirm_title" translatable="false">All set</string>
+    <!-- Subtitle shown on the confirmation screen after successful gesture. [CHAR LIMIT=60] -->
+    <string name="back_gesture_tutorial_confirm_subtitle" translatable="false">To change the sensitivity of the back gesture, go to Settings</string>
+
+    <!-- Button text shown on a button on the confirm screen. [CHAR LIMIT=14] -->
+    <string name="back_gesture_tutorial_action_button_label" translatable="false">Done</string>
+    <!-- Button text shown on a text button on the confirm screen. [CHAR LIMIT=14] -->
+    <string name="back_gesture_tutorial_action_text_button_label" translatable="false">Settings</string>
 </resources>
\ No newline at end of file
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index bb364ff..c8d7777 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -25,4 +25,39 @@
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">wrap_content</item>
     </style>
+
+    <style name="TextAppearance.BackGestureTutorial"
+        parent="android:TextAppearance.Material.Body1" />
+
+    <style name="TextAppearance.BackGestureTutorial.CallToAction"
+        parent="android:TextAppearance.Material.Body2" />
+
+    <style name="TextAppearance.BackGestureTutorial.Title"
+        parent="TextAppearance.BackGestureTutorial">
+        <item name="android:gravity">center</item>
+        <item name="android:textColor">@color/back_gesture_tutorial_title_color</item>
+        <item name="android:textSize">28sp</item>
+    </style>
+
+    <style name="TextAppearance.BackGestureTutorial.Subtitle"
+        parent="TextAppearance.BackGestureTutorial">
+        <item name="android:gravity">center</item>
+        <item name="android:textColor">@color/back_gesture_tutorial_subtitle_color</item>
+        <item name="android:letterSpacing">0.03</item>
+        <item name="android:textSize">21sp</item>
+    </style>
+
+    <style name="TextAppearance.BackGestureTutorial.ButtonLabel"
+        parent="TextAppearance.BackGestureTutorial.CallToAction">
+        <item name="android:gravity">center</item>
+        <item name="android:textColor">@color/back_gesture_tutorial_action_button_label_color</item>
+        <item name="android:letterSpacing">0.02</item>
+        <item name="android:textSize">16sp</item>
+        <item name="android:textAllCaps">false</item>
+    </style>
+
+    <style name="TextAppearance.BackGestureTutorial.TextButtonLabel"
+        parent="TextAppearance.BackGestureTutorial.ButtonLabel">
+        <item name="android:textColor">@color/back_gesture_tutorial_primary_color</item>
+    </style>
 </resources>
\ 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/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 8e5ed1a..a466f12 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -17,30 +17,34 @@
 package com.android.quickstep.logging;
 
 import static android.stats.launcher.nano.Launcher.ALLAPPS;
+import static android.stats.launcher.nano.Launcher.BACKGROUND;
+import static android.stats.launcher.nano.Launcher.DISMISS_TASK;
 import static android.stats.launcher.nano.Launcher.HOME;
 import static android.stats.launcher.nano.Launcher.LAUNCH_APP;
 import static android.stats.launcher.nano.Launcher.LAUNCH_TASK;
-import static android.stats.launcher.nano.Launcher.DISMISS_TASK;
-import static android.stats.launcher.nano.Launcher.BACKGROUND;
 import static android.stats.launcher.nano.Launcher.OVERVIEW;
 
 import android.content.Context;
 import android.content.Intent;
+import android.os.UserHandle;
 import android.stats.launcher.nano.Launcher;
 import android.stats.launcher.nano.LauncherExtension;
 import android.stats.launcher.nano.LauncherTarget;
 import android.util.Log;
 import android.view.View;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogUtils;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 import com.android.launcher3.util.ComponentKey;
 import com.android.systemui.shared.system.StatsLogCompat;
+
 import com.google.protobuf.nano.MessageNano;
 
 /**
@@ -60,7 +64,7 @@
     public StatsLogCompatManager(Context context) { }
 
     @Override
-    public void logAppLaunch(View v, Intent intent) {
+    public void logAppLaunch(View v, Intent intent, @Nullable UserHandle userHandle) {
         LauncherExtension ext = new LauncherExtension();
         ext.srcTarget = new LauncherTarget[SUPPORTED_TARGET_DEPTH];
         int srcState = mStateProvider.getCurrentState();
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index d49ff89..b249f48 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -15,6 +15,8 @@
  */
 package com.android.quickstep.util;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
+
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.content.Context;
@@ -26,7 +28,6 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.quickstep.SysUINavigationMode;
 
 import java.lang.annotation.Retention;
@@ -58,7 +59,7 @@
         } else {
             Resources res = context.getResources();
 
-            if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+            if (ENABLE_OVERVIEW_ACTIONS.get()) {
                 //TODO: this needs to account for the swipe gesture height and accessibility
                 // UI when shown.
                 extraSpace = 0;
@@ -111,7 +112,7 @@
             final int paddingResId;
             if (dp.isVerticalBarLayout()) {
                 paddingResId = R.dimen.landscape_task_card_horz_space;
-            } else if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+            } else if (ENABLE_OVERVIEW_ACTIONS.get()) {
                 paddingResId = R.dimen.portrait_task_card_horz_space_big_overview;
             } else {
                 paddingResId = R.dimen.portrait_task_card_horz_space;
@@ -146,6 +147,11 @@
 
     public static int getShelfTrackingDistance(Context context, DeviceProfile dp) {
         // Track the bottom of the window.
+        if (ENABLE_OVERVIEW_ACTIONS.get()) {
+            Rect taskSize = new Rect();
+            calculateLauncherTaskSize(context, dp, taskSize);
+            return (dp.heightPx - taskSize.height()) / 2;
+        }
         int shelfHeight = dp.hotseatBarSizePx + dp.getInsets().bottom;
         int spaceBetweenShelfAndRecents = (int) context.getResources().getDimension(
                 R.dimen.task_card_vert_space);
@@ -157,7 +163,7 @@
      * @return the margin in pixels.
      */
     public static int thumbnailBottomMargin(Resources resources) {
-        if (FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get()) {
+        if (ENABLE_OVERVIEW_ACTIONS.get()) {
             return resources.getDimensionPixelSize(R.dimen.overview_actions_height);
         } else {
             return 0;
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index 5606ac2..b786c8b 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -35,6 +35,7 @@
 
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.tapl.TestHelpers;
+import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.FailureWatcher;
 import com.android.systemui.shared.system.QuickStepContract;
 
@@ -57,6 +58,8 @@
 
     static final String TAG = "QuickStepOnOffRule";
 
+    public static final int WAIT_TIME_MS = 10000;
+
     public enum Mode {
         THREE_BUTTON, TWO_BUTTON, ZERO_BUTTON, ALL
     }
@@ -118,8 +121,8 @@
                         if (mode == THREE_BUTTON || mode == ALL) {
                             evaluateWithThreeButtons();
                         }
-                    } catch (Exception e) {
-                        Log.e(TAG, "Exception", e);
+                    } catch (Throwable e) {
+                        Log.e(TAG, "Error", e);
                         throw e;
                     } finally {
                         assertTrue("Couldn't set overlay",
@@ -195,19 +198,14 @@
                                 currentSysUiNavigationMode() == expectedMode);
                     }
 
-                    for (int i = 0; i != 100; ++i) {
-                        if (mLauncher.getNavigationModel() == expectedMode) break;
-                        Thread.sleep(100);
-                    }
-                    assertTrue("Couldn't switch to " + overlayPackage,
-                            mLauncher.getNavigationModel() == expectedMode);
+                    Wait.atMost("Couldn't switch to " + overlayPackage,
+                            () -> mLauncher.getNavigationModel() == expectedMode, WAIT_TIME_MS,
+                            mLauncher);
 
-                    for (int i = 0; i != 100; ++i) {
-                        if (mLauncher.getNavigationModeMismatchError() == null) break;
-                        Thread.sleep(100);
-                    }
-                    final String error = mLauncher.getNavigationModeMismatchError();
-                    assertTrue("Switching nav mode: " + error, error == null);
+                    Wait.atMost(() -> "Switching nav mode: "
+                                    + mLauncher.getNavigationModeMismatchError(),
+                            () -> mLauncher.getNavigationModeMismatchError() == null, WAIT_TIME_MS,
+                            mLauncher);
 
                     return true;
                 }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 428e647..ab6393a 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -100,6 +100,7 @@
     @PortraitLandscape
     public void testOverview() throws Exception {
         startTestApps();
+        // mLauncher.pressHome() also tests an important case of pressing home while in background.
         Overview overview = mLauncher.pressHome().switchToOverview();
         assertTrue("Launcher internal state didn't switch to Overview",
                 isInState(LauncherState.OVERVIEW));
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 3c8fe1e..815ae21 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -37,4 +37,10 @@
 
     <color name="all_apps_bg_hand_fill">#E5E5E5</color>
     <color name="all_apps_bg_hand_fill_dark">#9AA0A6</color>
+
+    <color name="back_gesture_tutorial_background_color">#FFFFFFFF</color>
+    <color name="back_gesture_tutorial_subtitle_color">#99000000</color> <!-- 60% black -->
+    <color name="back_gesture_tutorial_title_color">#FF000000</color>
+    <color name="back_gesture_tutorial_action_button_label_color">#FFFFFFFF</color>
+    <color name="back_gesture_tutorial_primary_color">#1A73E8</color> <!-- Blue -->
 </resources>
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/Android.mk b/robolectric_tests/Android.mk
index 310d43c..86a6e8c 100644
--- a/robolectric_tests/Android.mk
+++ b/robolectric_tests/Android.mk
@@ -19,6 +19,8 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := LauncherRoboTests
+LOCAL_MODULE_CLASS := JAVA_LIBRARIES
+
 LOCAL_SDK_VERSION := current
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 LOCAL_STATIC_JAVA_LIBRARIES := \
@@ -34,6 +36,9 @@
 LOCAL_INSTRUMENTATION_FOR := Launcher3
 LOCAL_MODULE_TAGS := optional
 
+# Generate test_config.properties
+include external/robolectric-shadows/gen_test_config.mk
+
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
 ############################################
@@ -43,14 +48,11 @@
 
 LOCAL_MODULE := RunLauncherRoboTests
 LOCAL_SDK_VERSION := current
-LOCAL_JAVA_LIBRARIES := \
-    LauncherRoboTests
+LOCAL_JAVA_LIBRARIES := LauncherRoboTests
 
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
 LOCAL_TEST_PACKAGE := Launcher3
-
-LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))../src \
+LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))../src
 
 LOCAL_ROBOTEST_TIMEOUT := 36000
 
diff --git a/robolectric_tests/config/robolectric.properties b/robolectric_tests/config/robolectric.properties
index e0d6e53..932b01b 100644
--- a/robolectric_tests/config/robolectric.properties
+++ b/robolectric_tests/config/robolectric.properties
@@ -1,2 +1 @@
-manifest=packages/apps/Launcher3/AndroidManifest.xml
-sdk=26
+sdk=28
diff --git a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideRule.java b/robolectric_tests/src/com/android/launcher3/config/FlagOverrideRule.java
index 4bb9a53..d33fecd 100644
--- a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideRule.java
+++ b/robolectric_tests/src/com/android/launcher3/config/FlagOverrideRule.java
@@ -14,6 +14,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.function.Function;
@@ -35,6 +36,8 @@
  */
 public final class FlagOverrideRule implements TestRule {
 
+    private final HashMap<String, Boolean> mDefaultOverrides = new HashMap<>();
+
     /**
      * Container annotation for handling multiple {@link FlagOverride} annotations.
      * <p>
@@ -60,6 +63,14 @@
         return new MyStatement(base, description);
     }
 
+    /**
+     * Sets a default override to apply on all tests
+     */
+    public FlagOverrideRule setOverride(BaseTogglableFlag flag, boolean value) {
+        mDefaultOverrides.put(flag.getKey(), value);
+        return this;
+    }
+
     private class MyStatement extends Statement {
 
         private final Statement mBase;
@@ -87,11 +98,15 @@
                         overrides = ((FlagOverrides) annotation).value();
                     }
                 }
-                for (FlagOverride override : overrides) {
-                    BaseTogglableFlag flag = allFlags.get(override.key());
+
+                HashMap<String, Boolean> allOverrides = new HashMap<>(mDefaultOverrides);
+                Arrays.stream(overrides).forEach(o -> allOverrides.put(o.key(), o.value()));
+
+                allOverrides.forEach((key, val) -> {
+                    BaseTogglableFlag flag = allFlags.get(key);
                     changedValues.put(flag, flag.get());
-                    flag.setForTests(override.value());
-                }
+                    flag.setForTests(val);
+                });
                 mBase.evaluate();
             } finally {
                 // Clear the values
diff --git a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java b/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java
index 31a037b..2a359df 100644
--- a/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java
+++ b/robolectric_tests/src/com/android/launcher3/config/FlagOverrideSampleTest.java
@@ -4,16 +4,16 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.launcher3.config.FlagOverrideRule.FlagOverride;
+import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
 
 /**
  * Sample Robolectric test that demonstrates flag-overriding.
  */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(LauncherRoboTestRunner.class)
 public class FlagOverrideSampleTest {
 
     // Check out https://junit.org/junit4/javadoc/4.12/org/junit/Rule.html for more information
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/logging/FileLogTest.java b/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java
index 410a077..48b5a45 100644
--- a/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java
+++ b/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java
@@ -3,11 +3,12 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import com.android.launcher3.util.LauncherRoboTestRunner;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.Shadows;
 import org.robolectric.util.Scheduler;
@@ -20,7 +21,7 @@
 /**
  * Tests for {@link FileLog}
  */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(LauncherRoboTestRunner.class)
 public class FileLogTest {
 
     private File mTempDir;
diff --git a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
index d7a2278..b7f2243 100644
--- a/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
@@ -4,54 +4,70 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
 import android.util.Pair;
 
+import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.ContentWriter;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
-import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 
 /**
  * Tests for {@link AddWorkspaceItemsTask}
  */
-@RunWith(RobolectricTestRunner.class)
-public class AddWorkspaceItemsTaskTest extends BaseModelUpdateTaskTestCase {
+@RunWith(LauncherRoboTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class AddWorkspaceItemsTaskTest {
 
     private final ComponentName mComponent1 = new ComponentName("a", "b");
     private final ComponentName mComponent2 = new ComponentName("b", "b");
 
-    private IntArray existingScreens;
-    private IntArray newScreens;
-    private IntSparseArrayMap<GridOccupancy> screenOccupancy;
+    private Context mTargetContext;
+    private InvariantDeviceProfile mIdp;
+    private LauncherAppState mAppState;
+    private LauncherModelHelper mModelHelper;
+
+    private IntArray mExistingScreens;
+    private IntArray mNewScreens;
+    private IntSparseArrayMap<GridOccupancy> mScreenOccupancy;
 
     @Before
-    public void initData() throws Exception {
-        existingScreens = new IntArray();
-        screenOccupancy = new IntSparseArrayMap<>();
-        newScreens = new IntArray();
+    public void setup() {
+        mModelHelper = new LauncherModelHelper();
+        mTargetContext = RuntimeEnvironment.application;
+        mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
+        mIdp.numColumns = mIdp.numRows = 5;
+        mAppState = LauncherAppState.getInstance(mTargetContext);
 
-        idp.numColumns = 5;
-        idp.numRows = 5;
+        mExistingScreens = new IntArray();
+        mScreenOccupancy = new IntSparseArrayMap<>();
+        mNewScreens = new IntArray();
     }
 
     private AddWorkspaceItemsTask newTask(ItemInfo... items) {
@@ -70,17 +86,17 @@
         // Second screen has 2 holes of sizes 3x2 and 2x3
         setupWorkspaceWithHoles(nextId, 2, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
 
-        int[] spaceFound = newTask()
-                .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 1, 1);
+        int[] spaceFound = newTask().findSpaceForItem(
+                mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 1, 1);
         assertEquals(2, spaceFound[0]);
-        assertTrue(screenOccupancy.get(spaceFound[0])
+        assertTrue(mScreenOccupancy.get(spaceFound[0])
                 .isRegionVacant(spaceFound[1], spaceFound[2], 1, 1));
 
         // Find a larger space
-        spaceFound = newTask()
-                .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 2, 3);
+        spaceFound = newTask().findSpaceForItem(
+                mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 2, 3);
         assertEquals(2, spaceFound[0]);
-        assertTrue(screenOccupancy.get(spaceFound[0])
+        assertTrue(mScreenOccupancy.get(spaceFound[0])
                 .isRegionVacant(spaceFound[1], spaceFound[2], 2, 3));
     }
 
@@ -89,11 +105,11 @@
         // First screen has 2 holes of sizes 3x2 and 2x3
         setupWorkspaceWithHoles(1, 1, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
 
-        IntArray oldScreens = existingScreens.clone();
-        int[] spaceFound = newTask()
-                .findSpaceForItem(appState, bgDataModel, existingScreens, newScreens, 3, 3);
+        IntArray oldScreens = mExistingScreens.clone();
+        int[] spaceFound = newTask().findSpaceForItem(
+                mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 3, 3);
         assertFalse(oldScreens.contains(spaceFound[0]));
-        assertTrue(newScreens.contains(spaceFound[0]));
+        assertTrue(mNewScreens.contains(spaceFound[0]));
     }
 
     @Test
@@ -105,11 +121,14 @@
         setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
 
         // Nothing was added
-        assertTrue(executeTaskForTest(newTask(info)).isEmpty());
+        assertTrue(mModelHelper.executeTaskForTest(newTask(info)).isEmpty());
     }
 
     @Test
     public void testAddItem_some_items_added() throws Exception {
+        Callbacks callbacks = mock(Callbacks.class);
+        mModelHelper.getModel().addCallbacks(callbacks);
+
         WorkspaceItemInfo info = new WorkspaceItemInfo();
         info.intent = new Intent().setComponent(mComponent1);
 
@@ -119,7 +138,7 @@
         // Setup a screen with a hole
         setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
 
-        executeTaskForTest(newTask(info, info2)).get(0).run();
+        mModelHelper.executeTaskForTest(newTask(info, info2)).get(0).run();
         ArgumentCaptor<ArrayList> notAnimated = ArgumentCaptor.forClass(ArrayList.class);
         ArgumentCaptor<ArrayList> animated = ArgumentCaptor.forClass(ArrayList.class);
 
@@ -134,18 +153,23 @@
     }
 
     private int setupWorkspaceWithHoles(int startId, int screenId, Rect... holes) throws Exception {
-        GridOccupancy occupancy = new GridOccupancy(idp.numColumns, idp.numRows);
-        occupancy.markCells(0, 0, idp.numColumns, idp.numRows, true);
+        return mModelHelper.executeSimpleTask(
+                model -> writeWorkspaceWithHoles(model, startId, screenId, holes));
+    }
+
+    private int writeWorkspaceWithHoles(
+            BgDataModel bgDataModel, int startId, int screenId, Rect... holes) {
+        GridOccupancy occupancy = new GridOccupancy(mIdp.numColumns, mIdp.numRows);
+        occupancy.markCells(0, 0, mIdp.numColumns, mIdp.numRows, true);
         for (Rect r : holes) {
             occupancy.markCells(r, false);
         }
 
-        existingScreens.add(screenId);
-        screenOccupancy.append(screenId, occupancy);
+        mExistingScreens.add(screenId);
+        mScreenOccupancy.append(screenId, occupancy);
 
-        ExecutorService executor = Executors.newSingleThreadExecutor();
-        for (int x = 0; x < idp.numColumns; x++) {
-            for (int y = 0; y < idp.numRows; y++) {
+        for (int x = 0; x < mIdp.numColumns; x++) {
+            for (int y = 0; y < mIdp.numRows; y++) {
                 if (!occupancy.cells[x][y]) {
                     continue;
                 }
@@ -157,20 +181,15 @@
                 info.cellX = x;
                 info.cellY = y;
                 info.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
-                bgDataModel.addItem(targetContext, info, false);
+                bgDataModel.addItem(mTargetContext, info, false);
 
-                executor.execute(() -> {
-                    ContentWriter writer = new ContentWriter(targetContext);
-                    info.writeToValues(writer);
-                    writer.put(Favorites._ID, info.id);
-                    targetContext.getContentResolver().insert(Favorites.CONTENT_URI,
-                            writer.getValues(targetContext));
-                });
+                ContentWriter writer = new ContentWriter(mTargetContext);
+                info.writeToValues(writer);
+                writer.put(Favorites._ID, info.id);
+                mTargetContext.getContentResolver().insert(Favorites.CONTENT_URI,
+                        writer.getValues(mTargetContext));
             }
         }
-
-        executor.submit(() -> null).get();
-        executor.shutdown();
         return startId;
     }
 }
diff --git a/robolectric_tests/src/com/android/launcher3/model/BaseGridChangesTestCase.java b/robolectric_tests/src/com/android/launcher3/model/BaseGridChangesTestCase.java
deleted file mode 100644
index 07834fc..0000000
--- a/robolectric_tests/src/com/android/launcher3/model/BaseGridChangesTestCase.java
+++ /dev/null
@@ -1,121 +0,0 @@
-package com.android.launcher3.model;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.database.sqlite.SQLiteDatabase;
-
-import com.android.launcher3.LauncherProvider;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.util.TestLauncherProvider;
-
-import org.junit.Before;
-import org.robolectric.Robolectric;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.shadows.ShadowContentResolver;
-import org.robolectric.shadows.ShadowLog;
-
-public abstract class BaseGridChangesTestCase {
-
-
-    public static final int DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP;
-    public static final int HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
-
-    public static final int APP_ICON = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
-    public static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
-    public static final int NO__ICON = -1;
-
-    public static final String TEST_PACKAGE = "com.android.launcher3.validpackage";
-
-    public Context mContext;
-    public TestLauncherProvider mProvider;
-    public SQLiteDatabase mDb;
-
-    @Before
-    public void setUpBaseCase() {
-        ShadowLog.stream = System.out;
-
-        mContext = RuntimeEnvironment.application;
-        mProvider = Robolectric.setupContentProvider(TestLauncherProvider.class);
-        ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, mProvider);
-        mDb = mProvider.getDb();
-    }
-
-    /**
-     * Adds a dummy item in the DB.
-     * @param type {@link #APP_ICON} or {@link #SHORTCUT} or >= 2 for
-     *             folder (where the type represents the number of items in the folder).
-     */
-    public int addItem(int type, int screen, int container, int x, int y) {
-        int id = LauncherSettings.Settings.call(mContext.getContentResolver(),
-                LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
-                .getInt(LauncherSettings.Settings.EXTRA_VALUE);
-
-        ContentValues values = new ContentValues();
-        values.put(LauncherSettings.Favorites._ID, id);
-        values.put(LauncherSettings.Favorites.CONTAINER, container);
-        values.put(LauncherSettings.Favorites.SCREEN, screen);
-        values.put(LauncherSettings.Favorites.CELLX, x);
-        values.put(LauncherSettings.Favorites.CELLY, y);
-        values.put(LauncherSettings.Favorites.SPANX, 1);
-        values.put(LauncherSettings.Favorites.SPANY, 1);
-
-        if (type == APP_ICON || type == SHORTCUT) {
-            values.put(LauncherSettings.Favorites.ITEM_TYPE, type);
-            values.put(LauncherSettings.Favorites.INTENT,
-                    new Intent(Intent.ACTION_MAIN).setPackage(TEST_PACKAGE).toUri(0));
-        } else {
-            values.put(LauncherSettings.Favorites.ITEM_TYPE,
-                    LauncherSettings.Favorites.ITEM_TYPE_FOLDER);
-            // Add folder items.
-            for (int i = 0; i < type; i++) {
-                addItem(APP_ICON, 0, id, 0, 0);
-            }
-        }
-
-        mContext.getContentResolver().insert(LauncherSettings.Favorites.CONTENT_URI, values);
-        return id;
-    }
-
-    public int[][][] createGrid(int[][][] typeArray) {
-        return createGrid(typeArray, 1);
-    }
-
-    /**
-     * Initializes the DB with dummy elements to represent the provided grid structure.
-     * @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for
-     *                  type definitions. The first dimension represents the screens and the next
-     *                  two represent the workspace grid.
-     * @param startScreen First screen id from where the icons will be added.
-     * @return the same grid representation where each entry is the corresponding item id.
-     */
-    public int[][][] createGrid(int[][][] typeArray, int startScreen) {
-        LauncherSettings.Settings.call(mContext.getContentResolver(),
-                LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
-        int[][][] ids = new int[typeArray.length][][];
-
-        for (int i = 0; i < typeArray.length; i++) {
-            // Add screen to DB
-            int screenId = startScreen + i;
-
-            // Keep the screen id counter up to date
-            LauncherSettings.Settings.call(mContext.getContentResolver(),
-                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
-
-            ids[i] = new int[typeArray[i].length][];
-            for (int y = 0; y < typeArray[i].length; y++) {
-                ids[i][y] = new int[typeArray[i][y].length];
-                for (int x = 0; x < typeArray[i][y].length; x++) {
-                    if (typeArray[i][y][x] < 0) {
-                        // Empty cell
-                        ids[i][y][x] = -1;
-                    } else {
-                        ids[i][y][x] = addItem(typeArray[i][y][x], screenId, DESKTOP, x, y);
-                    }
-                }
-            }
-        }
-
-        return ids;
-    }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java b/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
deleted file mode 100644
index 012258d..0000000
--- a/robolectric_tests/src/com/android/launcher3/model/BaseModelUpdateTaskTestCase.java
+++ /dev/null
@@ -1,231 +0,0 @@
-package com.android.launcher3.model;
-
-import static com.android.launcher3.shadows.ShadowLooperExecutor.reinitializeStaticExecutors;
-
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Mockito.atLeast;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
-import android.graphics.Color;
-import android.os.Process;
-import android.os.UserHandle;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.AppFilter;
-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;
-import com.android.launcher3.LauncherModel.ModelUpdateTask;
-import com.android.launcher3.LauncherProvider;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.icons.cache.CachingLogic;
-import com.android.launcher3.model.BgDataModel.Callbacks;
-import com.android.launcher3.pm.InstallSessionHelper;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.TestLauncherProvider;
-
-import org.junit.Before;
-import org.mockito.ArgumentCaptor;
-import org.robolectric.Robolectric;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.shadows.ShadowContentResolver;
-import org.robolectric.shadows.ShadowLog;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-import java.lang.reflect.Field;
-import java.util.HashMap;
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.function.Supplier;
-
-/**
- * Base class for writing tests for Model update tasks.
- */
-public class BaseModelUpdateTaskTestCase {
-
-    public final HashMap<Class, HashMap<String, Field>> fieldCache = new HashMap<>();
-    public TestLauncherProvider provider;
-
-    public Context targetContext;
-    public UserHandle myUser;
-
-    public InvariantDeviceProfile idp;
-    public LauncherAppState appState;
-    public LauncherModel model;
-    public ModelWriter modelWriter;
-    public MyIconCache iconCache;
-
-    public BgDataModel bgDataModel;
-    public AllAppsList allAppsList;
-    public Callbacks callbacks;
-
-    @Before
-    public void setUp() throws Exception {
-        ShadowLog.stream = System.out;
-        reinitializeStaticExecutors();
-        InstallSessionHelper.INSTANCE.initializeForTesting(null);
-
-        provider = Robolectric.setupContentProvider(TestLauncherProvider.class);
-        ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, provider);
-
-        callbacks = mock(Callbacks.class);
-        appState = mock(LauncherAppState.class);
-        model = mock(LauncherModel.class);
-        modelWriter = mock(ModelWriter.class);
-
-        LauncherAppState.INSTANCE.initializeForTesting(appState);
-        when(appState.getModel()).thenReturn(model);
-        when(model.getWriter(anyBoolean(), anyBoolean())).thenReturn(modelWriter);
-        when(model.getCallback()).thenReturn(callbacks);
-
-        myUser = Process.myUserHandle();
-
-        bgDataModel = new BgDataModel();
-        targetContext = RuntimeEnvironment.application;
-
-        idp = new InvariantDeviceProfile();
-        iconCache = new MyIconCache(targetContext, idp);
-
-        allAppsList = new AllAppsList(iconCache, new AppFilter());
-
-        when(appState.getIconCache()).thenReturn(iconCache);
-        when(appState.getInvariantDeviceProfile()).thenReturn(idp);
-        when(appState.getContext()).thenReturn(targetContext);
-    }
-
-    /**
-     * Synchronously executes the task and returns all the UI callbacks posted.
-     */
-    public List<Runnable> executeTaskForTest(ModelUpdateTask task) throws Exception {
-        when(model.isModelLoaded()).thenReturn(true);
-
-        Executor mockExecutor = mock(Executor.class);
-
-        task.init(appState, model, bgDataModel, allAppsList, mockExecutor);
-        task.run();
-        ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
-        verify(mockExecutor, atLeast(0)).execute(captor.capture());
-
-        return captor.getAllValues();
-    }
-
-    /**
-     * Initializes mock data for the test.
-     */
-    public void initializeData(String resourceName) throws Exception {
-        try (BufferedReader reader = new BufferedReader(new InputStreamReader(
-                this.getClass().getResourceAsStream(resourceName)))) {
-            String line;
-            HashMap<String, Class> classMap = new HashMap<>();
-            while((line = reader.readLine()) != null) {
-                line = line.trim();
-                if (line.startsWith("#") || line.isEmpty()) {
-                    continue;
-                }
-                String[] commands = line.split(" ");
-                switch (commands[0]) {
-                    case "classMap":
-                        classMap.put(commands[1], Class.forName(commands[2]));
-                        break;
-                    case "bgItem":
-                        bgDataModel.addItem(targetContext,
-                                (ItemInfo) initItem(classMap.get(commands[1]), commands, 2), false);
-                        break;
-                    case "allApps":
-                        allAppsList.add((AppInfo) initItem(AppInfo.class, commands, 1), null);
-                        break;
-                }
-            }
-        }
-    }
-
-    private Object initItem(Class clazz, String[] fieldDef, int startIndex) throws Exception {
-        HashMap<String, Field> cache = fieldCache.get(clazz);
-        if (cache == null) {
-            cache = new HashMap<>();
-            Class c = clazz;
-            while (c != null) {
-                for (Field f : c.getDeclaredFields()) {
-                    f.setAccessible(true);
-                    cache.put(f.getName(), f);
-                }
-                c = c.getSuperclass();
-            }
-            fieldCache.put(clazz, cache);
-        }
-
-        Object item = clazz.newInstance();
-        for (int i = startIndex; i < fieldDef.length; i++) {
-            String[] fieldData = fieldDef[i].split("=", 2);
-            Field f = cache.get(fieldData[0]);
-            Class type = f.getType();
-            if (type == int.class || type == long.class) {
-                f.set(item, Integer.parseInt(fieldData[1]));
-            } else if (type == CharSequence.class || type == String.class) {
-                f.set(item, fieldData[1]);
-            } else if (type == Intent.class) {
-                if (!fieldData[1].startsWith("#Intent")) {
-                    fieldData[1] = "#Intent;" + fieldData[1] + ";end";
-                }
-                f.set(item, Intent.parseUri(fieldData[1], 0));
-            } else if (type == ComponentName.class) {
-                f.set(item, ComponentName.unflattenFromString(fieldData[1]));
-            } else {
-                throw new Exception("Added parsing logic for "
-                        + f.getName() + " of type " + f.getType());
-            }
-        }
-        return item;
-    }
-
-    public static class MyIconCache extends IconCache {
-
-        private final HashMap<ComponentKey, CacheEntry> mCache = new HashMap<>();
-
-        public MyIconCache(Context context, InvariantDeviceProfile idp) {
-            super(context, idp);
-        }
-
-        @Override
-        protected <T> CacheEntry cacheLocked(
-                @NonNull ComponentName componentName,
-                UserHandle user, @NonNull Supplier<T> infoProvider,
-                @NonNull CachingLogic<T> cachingLogic,
-                boolean usePackageIcon, boolean useLowResIcon) {
-            CacheEntry entry = mCache.get(new ComponentKey(componentName, user));
-            if (entry == null) {
-                entry = new CacheEntry();
-                entry.bitmap = getDefaultIcon(user);
-            }
-            return entry;
-        }
-
-        public void addCache(ComponentName key, String title) {
-            CacheEntry entry = new CacheEntry();
-            entry.bitmap = BitmapInfo.of(newIcon(), Color.RED);
-            entry.title = title;
-            mCache.put(new ComponentKey(key, Process.myUserHandle()), entry);
-        }
-
-        public Bitmap newIcon() {
-            return Bitmap.createBitmap(1, 1, Config.ARGB_8888);
-        }
-
-        @Override
-        public synchronized BitmapInfo getDefaultIcon(UserHandle user) {
-            return BitmapInfo.fromBitmap(newIcon());
-        }
-    }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
index 69c5b00..f128e24 100644
--- a/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -5,15 +5,34 @@
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertTrue;
 
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Color;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.icons.cache.CachingLogic;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
 
 import java.util.Arrays;
 import java.util.HashSet;
@@ -21,40 +40,73 @@
 /**
  * Tests for {@link CacheDataUpdatedTask}
  */
-@RunWith(RobolectricTestRunner.class)
-public class CacheDataUpdatedTaskTest extends BaseModelUpdateTaskTestCase {
+@RunWith(LauncherRoboTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class CacheDataUpdatedTaskTest {
 
     private static final String NEW_LABEL_PREFIX = "new-label-";
 
+    private LauncherModelHelper mModelHelper;
+
     @Before
-    public void initData() throws Exception {
-        initializeData("/cache_data_updated_task_data.txt");
+    public void setup() throws Exception {
+        mModelHelper = new LauncherModelHelper();
+        mModelHelper.initializeData("/cache_data_updated_task_data.txt");
+
         // Add dummy entries in the cache to simulate update
-        for (ItemInfo info : bgDataModel.itemsIdMap) {
-            iconCache.addCache(info.getTargetComponent(), NEW_LABEL_PREFIX + info.id);
+        Context context = RuntimeEnvironment.application;
+        IconCache iconCache = LauncherAppState.getInstance(context).getIconCache();
+        CachingLogic<ItemInfo> dummyLogic = new CachingLogic<ItemInfo>() {
+            @Override
+            public ComponentName getComponent(ItemInfo info) {
+                return info.getTargetComponent();
+            }
+
+            @Override
+            public UserHandle getUser(ItemInfo info) {
+                return info.user;
+            }
+
+            @Override
+            public CharSequence getLabel(ItemInfo info) {
+                return NEW_LABEL_PREFIX + info.id;
+            }
+
+            @NonNull
+            @Override
+            public BitmapInfo loadIcon(Context context, ItemInfo info) {
+                return BitmapInfo.of(Bitmap.createBitmap(1, 1, Config.ARGB_8888), Color.RED);
+            }
+        };
+
+        UserManager um = context.getSystemService(UserManager.class);
+        for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
+            iconCache.addIconToDBAndMemCache(info, dummyLogic, new PackageInfo(),
+                    um.getSerialNumberForUser(info.user), true);
         }
     }
 
     private CacheDataUpdatedTask newTask(int op, String... pkg) {
-        return new CacheDataUpdatedTask(op, myUser, new HashSet<>(Arrays.asList(pkg)));
+        return new CacheDataUpdatedTask(op, Process.myUserHandle(),
+                new HashSet<>(Arrays.asList(pkg)));
     }
 
     @Test
     public void testCacheUpdate_update_apps() throws Exception {
         // Clear all icons from apps list so that its easy to check what was updated
-        for (AppInfo info : allAppsList.data) {
+        for (AppInfo info : mModelHelper.getAllAppsList().data) {
             info.bitmap = BitmapInfo.LOW_RES_INFO;
         }
 
-        executeTaskForTest(newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, "app1"));
+        mModelHelper.executeTaskForTest(newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, "app1"));
 
         // Verify that only the app icons of app1 (id 1 & 2) are updated. Custom shortcut (id 7)
         // is not updated
         verifyUpdate(1, 2);
 
         // Verify that only app1 var updated in allAppsList
-        assertFalse(allAppsList.data.isEmpty());
-        for (AppInfo info : allAppsList.data) {
+        assertFalse(mModelHelper.getAllAppsList().data.isEmpty());
+        for (AppInfo info : mModelHelper.getAllAppsList().data) {
             if (info.componentName.getPackageName().equals("app1")) {
                 assertFalse(info.bitmap.isNullOrLowRes());
             } else {
@@ -65,7 +117,7 @@
 
     @Test
     public void testSessionUpdate_ignores_normal_apps() throws Exception {
-        executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app1"));
+        mModelHelper.executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app1"));
 
         // app1 has no restored shortcuts. Verify that nothing was updated.
         verifyUpdate();
@@ -73,7 +125,7 @@
 
     @Test
     public void testSessionUpdate_updates_pending_apps() throws Exception {
-        executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app3"));
+        mModelHelper.executeTaskForTest(newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, "app3"));
 
         // app3 has only restored apps (id 5, 6) and shortcuts (id 9). Verify that only apps were
         // were updated
@@ -82,7 +134,7 @@
 
     private void verifyUpdate(Integer... idsUpdated) {
         HashSet<Integer> updates = new HashSet<>(Arrays.asList(idsUpdated));
-        for (ItemInfo info : bgDataModel.itemsIdMap) {
+        for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
             if (updates.contains(info.id)) {
                 assertEquals(NEW_LABEL_PREFIX + info.id, info.title);
                 assertFalse(((WorkspaceItemInfo) info).bitmap.isNullOrLowRes());
diff --git a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java b/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
index b7340cf..1442c55 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
@@ -36,11 +36,11 @@
 import com.android.launcher3.LauncherProvider.DatabaseHelper;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.R;
+import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 
 import java.io.File;
@@ -48,7 +48,7 @@
 /**
  * Tests for {@link DbDowngradeHelper}
  */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(LauncherRoboTestRunner.class)
 public class DbDowngradeHelperTest {
 
     private static final String SCHEMA_FILE = "test_schema.json";
diff --git a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
index 68713d8..f8ac010 100644
--- a/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/DefaultLayoutProviderTest.java
@@ -16,97 +16,65 @@
 
 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;
 
 import android.content.ComponentName;
+import android.content.Context;
 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.LauncherProvider;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.pm.InstallSessionHelper;
-import com.android.launcher3.shadows.LShadowLauncherApps;
-import com.android.launcher3.shadows.LShadowUserManager;
-import com.android.launcher3.shadows.ShadowLooperExecutor;
+import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.LauncherLayoutBuilder;
-import com.android.launcher3.widget.custom.CustomWidgetManager;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
+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;
 
 /**
  * Tests for layout parser for remote layout
  */
-@RunWith(RobolectricTestRunner.class)
-@Config(shadows = {LShadowUserManager.class, LShadowLauncherApps.class, ShadowLooperExecutor.class})
+@RunWith(LauncherRoboTestRunner.class)
 @LooperMode(Mode.PAUSED)
-public class DefaultLayoutProviderTest extends BaseModelUpdateTaskTestCase {
+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;
 
     @Before
-    public void setUp() throws Exception {
-        super.setUp();
-        InvariantDeviceProfile.INSTANCE.initializeForTesting(idp);
-        CustomWidgetManager.INSTANCE.initializeForTesting(mock(CustomWidgetManager.class));
+    public void setUp() {
+        mModelHelper = new LauncherModelHelper();
+        mTargetContext = RuntimeEnvironment.application;
 
-        idp.numRows = idp.numColumns = idp.numHotseatIcons = GRID_SIZE;
-        idp.iconBitmapSize = BITMAP_SIZE;
-
-        provider.setAllowLoadDefaultFavorites(true);
-        Settings.Secure.putString(targetContext.getContentResolver(),
-                "launcher3.layout.provider", TEST_PROVIDER_AUTHORITY);
-
-        ShadowPackageManager spm = shadowOf(targetContext.getPackageManager());
-        spm.addProviderIfNotPresent(new ComponentName("com.test", "Dummy")).authority =
-                TEST_PROVIDER_AUTHORITY;
-        spm.addActivityIfNotPresent(new ComponentName(SETTINGS_APP, SETTINGS_APP));
-    }
-
-    @After
-    public void cleanup() {
-        InvariantDeviceProfile.INSTANCE.initializeForTesting(null);
-        CustomWidgetManager.INSTANCE.initializeForTesting(null);
-        InstallSessionHelper.INSTANCE.initializeForTesting(null);
+        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, bgDataModel.workspaceItems.size());
-        ItemInfo info = bgDataModel.workspaceItems.get(0);
+        assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
+        ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
         assertEquals(LauncherSettings.Favorites.CONTAINER_HOTSEAT, info.container);
         assertEquals(LauncherSettings.Favorites.ITEM_TYPE_APPLICATION, info.itemType);
     }
@@ -114,14 +82,14 @@
     @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
-        assertEquals(1, bgDataModel.workspaceItems.size());
-        ItemInfo info = bgDataModel.workspaceItems.get(0);
+        assertEquals(1, mModelHelper.getBgDataModel().workspaceItems.size());
+        ItemInfo info = mModelHelper.getBgDataModel().workspaceItems.get(0);
         assertEquals(LauncherSettings.Favorites.ITEM_TYPE_FOLDER, info.itemType);
         assertEquals(3, ((FolderInfo) info).contents.size());
     }
@@ -134,7 +102,7 @@
         SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
         params.setAppPackageName(pendingAppPkg);
 
-        PackageInstaller installer = targetContext.getPackageManager().getPackageInstaller();
+        PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller();
         int sessionId = installer.createSession(params);
         SessionInfo sessionInfo = installer.getSessionInfo(sessionId);
         setField(sessionInfo, "installerPackageName", "com.test");
@@ -144,24 +112,26 @@
                 .putWidget(pendingAppPkg, "DummyWidget", 2, 2));
 
         // Verify widget
-        assertEquals(1, bgDataModel.appWidgets.size());
-        ItemInfo info = bgDataModel.appWidgets.get(0);
+        assertEquals(1, mModelHelper.getBgDataModel().appWidgets.size());
+        ItemInfo info = mModelHelper.getBgDataModel().appWidgets.get(0);
         assertEquals(LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET, info.itemType);
         assertEquals(2, info.spanX);
         assertEquals(2, info.spanY);
     }
 
     private void writeLayoutAndLoad(LauncherLayoutBuilder builder) throws Exception {
-        ByteArrayOutputStream bos = new ByteArrayOutputStream();
-        builder.build(new OutputStreamWriter(bos));
+        mModelHelper.setupDefaultLayoutProvider(builder);
 
-        Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, targetContext);
-        shadowOf(targetContext.getContentResolver()).registerInputStream(layoutUri,
-                new ByteArrayInputStream(bos.toByteArray()));
-
-        LoaderResults results = new LoaderResults(appState, bgDataModel, allAppsList, 0,
-                new WeakReference<>(callbacks));
-        LoaderTask task = new LoaderTask(appState, allAppsList, bgDataModel, results);
+        LoaderResults results = new LoaderResults(
+                LauncherAppState.getInstance(mTargetContext),
+                mModelHelper.getBgDataModel(),
+                mModelHelper.getAllAppsList(),
+                new Callbacks[0]);
+        LoaderTask task = new LoaderTask(
+                LauncherAppState.getInstance(mTargetContext),
+                mModelHelper.getAllAppsList(),
+                mModelHelper.getBgDataModel(),
+                results);
         Executors.MODEL_EXECUTOR.submit(() -> task.loadWorkspace(new ArrayList<>())).get();
     }
 }
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java b/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java
index 53287a9..f46b849 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridBackupTableTest.java
@@ -6,33 +6,53 @@
 import static com.android.launcher3.LauncherSettings.Favorites.BACKUP_TABLE_NAME;
 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
+import static com.android.launcher3.util.LauncherModelHelper.APP_ICON;
+import static com.android.launcher3.util.LauncherModelHelper.DESKTOP;
+import static com.android.launcher3.util.LauncherModelHelper.NO__ICON;
+import static com.android.launcher3.util.LauncherModelHelper.SHORTCUT;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import android.content.ContentValues;
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Point;
 
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherSettings.Settings;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
 
 /**
  * Unit tests for {@link GridBackupTable}
  */
-@RunWith(RobolectricTestRunner.class)
-public class GridBackupTableTest extends BaseGridChangesTestCase {
+@RunWith(LauncherRoboTestRunner.class)
+public class GridBackupTableTest {
 
     private static final int BACKUP_ITEM_COUNT = 12;
 
+    private LauncherModelHelper mModelHelper;
+    private Context mContext;
+    private SQLiteDatabase mDb;
+
     @Before
-    public void setupGridData() {
-        createGrid(new int[][][]{{
+    public void setUp() {
+        mModelHelper = new LauncherModelHelper();
+        mContext = RuntimeEnvironment.application;
+        mDb = mModelHelper.provider.getDb();
+
+        setupGridData();
+    }
+
+    private void setupGridData() {
+        mModelHelper.createGrid(new int[][][]{{
                 { APP_ICON, APP_ICON, SHORTCUT, SHORTCUT},
                 { SHORTCUT, SHORTCUT, NO__ICON, NO__ICON},
                 { NO__ICON, NO__ICON, SHORTCUT, SHORTCUT},
@@ -81,7 +101,7 @@
 
         assertTrue(tableExists(mDb, BACKUP_TABLE_NAME));
 
-        addItem(1, 2, DESKTOP, 1, 1);
+        mModelHelper.addItem(1, 2, DESKTOP, 1, 1);
         assertFalse(tableExists(mDb, BACKUP_TABLE_NAME));
     }
 
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
index 53f6a06..1ed4bca 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskTest.java
@@ -1,25 +1,31 @@
 package com.android.launcher3.model;
 
 import static com.android.launcher3.model.GridSizeMigrationTask.getWorkspaceScreenIds;
+import static com.android.launcher3.util.LauncherModelHelper.APP_ICON;
+import static com.android.launcher3.util.LauncherModelHelper.HOTSEAT;
+import static com.android.launcher3.util.LauncherModelHelper.SHORTCUT;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import android.content.Context;
 import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Point;
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.config.FlagOverrideRule;
 import com.android.launcher3.model.GridSizeMigrationTask.MultiStepMigrationTask;
 import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
 
 import java.util.HashSet;
 import java.util.LinkedList;
@@ -27,30 +33,35 @@
 /**
  * Unit tests for {@link GridSizeMigrationTask}
  */
-@RunWith(RobolectricTestRunner.class)
-public class GridSizeMigrationTaskTest extends BaseGridChangesTestCase {
+@RunWith(LauncherRoboTestRunner.class)
+public class GridSizeMigrationTaskTest {
 
-    @Rule
-    public final FlagOverrideRule flags = new FlagOverrideRule();
+    private LauncherModelHelper mModelHelper;
+    private Context mContext;
+    private SQLiteDatabase mDb;
 
     private HashSet<String> mValidPackages;
     private InvariantDeviceProfile mIdp;
 
     @Before
     public void setUp() {
+        mModelHelper = new LauncherModelHelper();
+        mContext = RuntimeEnvironment.application;
+        mDb = mModelHelper.provider.getDb();
+
         mValidPackages = new HashSet<>();
         mValidPackages.add(TEST_PACKAGE);
-        mIdp = new InvariantDeviceProfile();
+        mIdp = InvariantDeviceProfile.INSTANCE.get(mContext);
     }
 
     @Test
     public void testHotseatMigration_apps_dropped() throws Exception {
         int[] hotseatItems = {
-                addItem(APP_ICON, 0, HOTSEAT, 0, 0),
-                addItem(SHORTCUT, 1, HOTSEAT, 0, 0),
+                mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0),
+                mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0),
                 -1,
-                addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
-                addItem(APP_ICON, 4, HOTSEAT, 0, 0),
+                mModelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
+                mModelHelper.addItem(APP_ICON, 4, HOTSEAT, 0, 0),
         };
 
         mIdp.numHotseatIcons = 3;
@@ -63,11 +74,11 @@
     @Test
     public void testHotseatMigration_shortcuts_dropped() throws Exception {
         int[] hotseatItems = {
-                addItem(APP_ICON, 0, HOTSEAT, 0, 0),
-                addItem(30, 1, HOTSEAT, 0, 0),
+                mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0),
+                mModelHelper.addItem(30, 1, HOTSEAT, 0, 0),
                 -1,
-                addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
-                addItem(10, 4, HOTSEAT, 0, 0),
+                mModelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
+                mModelHelper.addItem(10, 4, HOTSEAT, 0, 0),
         };
 
         mIdp.numHotseatIcons = 3;
@@ -109,7 +120,7 @@
 
     @Test
     public void testWorkspace_empty_row_column_removed() throws Exception {
-        int[][][] ids = createGrid(new int[][][]{{
+        int[][][] ids = mModelHelper.createGrid(new int[][][]{{
                 {  0,  0, -1,  1},
                 {  3,  1, -1,  4},
                 { -1, -1, -1, -1},
@@ -129,7 +140,7 @@
 
     @Test
     public void testWorkspace_new_screen_created() throws Exception {
-        int[][][] ids = createGrid(new int[][][]{{
+        int[][][] ids = mModelHelper.createGrid(new int[][][]{{
                 {  0,  0,  0,  1},
                 {  3,  1,  0,  4},
                 { -1, -1, -1, -1},
@@ -151,7 +162,7 @@
 
     @Test
     public void testWorkspace_items_merged_in_next_screen() throws Exception {
-        int[][][] ids = createGrid(new int[][][]{{
+        int[][][] ids = mModelHelper.createGrid(new int[][][]{{
                 {  0,  0,  0,  1},
                 {  3,  1,  0,  4},
                 { -1, -1, -1, -1},
@@ -179,9 +190,9 @@
 
     @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 = createGrid(new int[][][]{{
+        int[][][] ids = mModelHelper.createGrid(new int[][][]{{
                 {  0,  0,  0,  1},
                 {  3,  1,  0,  4},
                 { -1, -1, -1, -1},
@@ -217,7 +228,7 @@
         }
         // The first screen has one item on the 4th column which needs moving, as the first row
         // will be kept empty.
-        int[][][] ids = createGrid(new int[][][]{{
+        int[][][] ids = mModelHelper.createGrid(new int[][][]{{
                 { -1, -1, -1, -1},
                 {  3,  1,  7,  0},
                 {  8,  7,  7, -1},
@@ -244,7 +255,7 @@
             return;
         }
         // Items will get moved to the next screen to keep the first screen empty.
-        int[][][] ids = createGrid(new int[][][]{{
+        int[][][] ids = mModelHelper.createGrid(new int[][][]{{
                 { -1, -1, -1, -1},
                 {  0,  1,  0,  0},
                 {  8,  7,  7, -1},
@@ -266,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/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
similarity index 74%
rename from tests/src/com/android/launcher3/model/LoaderCursorTest.java
rename to robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
index 0dcfaa8..7fa3ee9 100644
--- a/tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.launcher3.model;
 
 import static com.android.launcher3.LauncherSettings.Favorites.CELLX;
@@ -17,6 +33,7 @@
 import static com.android.launcher3.LauncherSettings.Favorites.SCREEN;
 import static com.android.launcher3.LauncherSettings.Favorites.TITLE;
 import static com.android.launcher3.LauncherSettings.Favorites._ID;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
@@ -24,69 +41,57 @@
 import static junit.framework.Assert.assertNull;
 import static junit.framework.Assert.assertTrue;
 
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
 
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.LauncherApps;
 import android.database.MatrixCursor;
-import android.graphics.Bitmap;
 import android.os.Process;
 
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.WorkspaceItemInfo;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.LauncherRoboTestRunner;
 import com.android.launcher3.util.PackageManagerHelper;
 
 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;
 
 /**
  * Tests for {@link LoaderCursor}
  */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(LauncherRoboTestRunner.class)
+@LooperMode(Mode.PAUSED)
 public class LoaderCursorTest {
 
-    private LauncherAppState mMockApp;
-    private IconCache mMockIconCache;
+    private LauncherAppState mApp;
 
     private MatrixCursor mCursor;
     private InvariantDeviceProfile mIDP;
     private Context mContext;
-    private LauncherApps mLauncherApps;
 
     private LoaderCursor mLoaderCursor;
 
     @Before
     public void setup() {
-        mIDP = new InvariantDeviceProfile();
+        mContext = RuntimeEnvironment.application;
+        mIDP = InvariantDeviceProfile.INSTANCE.get(mContext);
+        mApp = LauncherAppState.getInstance(mContext);
+
         mCursor = new MatrixCursor(new String[] {
                 ICON, ICON_PACKAGE, ICON_RESOURCE, TITLE,
                 _ID, CONTAINER, ITEM_TYPE, PROFILE_ID,
                 SCREEN, CELLX, CELLY, RESTORED, INTENT
         });
-        mContext = InstrumentationRegistry.getTargetContext();
 
-        mMockApp = mock(LauncherAppState.class);
-        mMockIconCache = mock(IconCache.class);
-        when(mMockApp.getIconCache()).thenReturn(mMockIconCache);
-        when(mMockApp.getInvariantDeviceProfile()).thenReturn(mIDP);
-        when(mMockApp.getContext()).thenReturn(mContext);
-        mLauncherApps = mContext.getSystemService(LauncherApps.class);
-
-        mLoaderCursor = new LoaderCursor(mCursor, mMockApp);
+        mLoaderCursor = new LoaderCursor(mCursor, mApp);
         mLoaderCursor.allUsers.put(0, Process.myUserHandle());
     }
 
@@ -109,26 +114,31 @@
     }
 
     @Test
-    public void getAppShortcutInfo_dontAllowMissing_validComponent() {
+    public void getAppShortcutInfo_dontAllowMissing_validComponent() throws Exception {
+        ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_PACKAGE);
+        shadowOf(mContext.getPackageManager()).addActivityIfNotPresent(cn);
+
         initCursor(ITEM_TYPE_APPLICATION, "");
         assertTrue(mLoaderCursor.moveToNext());
 
-        ComponentName cn = mLauncherApps.getActivityList(null, mLoaderCursor.user)
-                .get(0).getComponentName();
-        WorkspaceItemInfo info = mLoaderCursor.getAppShortcutInfo(
-                new Intent().setComponent(cn), false /* allowMissingTarget */, true);
+        WorkspaceItemInfo info = Executors.MODEL_EXECUTOR.submit(() ->
+                mLoaderCursor.getAppShortcutInfo(
+                        new Intent().setComponent(cn), false  /* allowMissingTarget */, true))
+                .get();
         assertNotNull(info);
         assertTrue(PackageManagerHelper.isLauncherAppTarget(info.intent));
     }
 
     @Test
-    public void getAppShortcutInfo_allowMissing_invalidComponent() {
+    public void getAppShortcutInfo_allowMissing_invalidComponent() throws Exception {
         initCursor(ITEM_TYPE_APPLICATION, "");
         assertTrue(mLoaderCursor.moveToNext());
 
         ComponentName cn = new ComponentName(mContext.getPackageName(), "dummy-do");
-        WorkspaceItemInfo info = mLoaderCursor.getAppShortcutInfo(
-                new Intent().setComponent(cn), true  /* allowMissingTarget */, true);
+        WorkspaceItemInfo info = Executors.MODEL_EXECUTOR.submit(() ->
+                mLoaderCursor.getAppShortcutInfo(
+                        new Intent().setComponent(cn), true  /* allowMissingTarget */, true))
+                .get();
         assertNotNull(info);
         assertTrue(PackageManagerHelper.isLauncherAppTarget(info.intent));
     }
@@ -138,11 +148,8 @@
         initCursor(ITEM_TYPE_SHORTCUT, "my-shortcut");
         assertTrue(mLoaderCursor.moveToNext());
 
-        Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
-        when(mMockIconCache.getDefaultIcon(eq(mLoaderCursor.user)))
-                .thenReturn(BitmapInfo.fromBitmap(icon));
         WorkspaceItemInfo info = mLoaderCursor.loadSimpleWorkspaceItem();
-        assertEquals(icon, info.bitmap.icon);
+        assertTrue(mApp.getIconCache().isDefaultIcon(info.bitmap, info.user));
         assertEquals("my-shortcut", info.title);
         assertEquals(ITEM_TYPE_SHORTCUT, info.itemType);
     }
@@ -164,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(
@@ -190,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/model/PackageInstallStateChangedTaskTest.java b/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
index a1a4561..bd71f01 100644
--- a/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/PackageInstallStateChangedTaskTest.java
@@ -6,11 +6,14 @@
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.pm.PackageInstallInfo;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.LooperMode;
+import org.robolectric.annotation.LooperMode.Mode;
 
 import java.util.Arrays;
 import java.util.HashSet;
@@ -18,12 +21,16 @@
 /**
  * Tests for {@link PackageInstallStateChangedTask}
  */
-@RunWith(RobolectricTestRunner.class)
-public class PackageInstallStateChangedTaskTest extends BaseModelUpdateTaskTestCase {
+@RunWith(LauncherRoboTestRunner.class)
+@LooperMode(Mode.PAUSED)
+public class PackageInstallStateChangedTaskTest {
+
+    private LauncherModelHelper mModelHelper;
 
     @Before
-    public void initData() throws Exception {
-        initializeData("/package_install_state_change_task_data.txt");
+    public void setup() throws Exception {
+        mModelHelper = new LauncherModelHelper();
+        mModelHelper.initializeData("/package_install_state_change_task_data.txt");
     }
 
     private PackageInstallStateChangedTask newTask(String pkg, int progress) {
@@ -35,7 +42,7 @@
 
     @Test
     public void testSessionUpdate_ignore_installed() throws Exception {
-        executeTaskForTest(newTask("app1", 30));
+        mModelHelper.executeTaskForTest(newTask("app1", 30));
 
         // No shortcuts were updated
         verifyProgressUpdate(0);
@@ -43,21 +50,21 @@
 
     @Test
     public void testSessionUpdate_shortcuts_updated() throws Exception {
-        executeTaskForTest(newTask("app3", 30));
+        mModelHelper.executeTaskForTest(newTask("app3", 30));
 
         verifyProgressUpdate(30, 5, 6, 7);
     }
 
     @Test
     public void testSessionUpdate_widgets_updated() throws Exception {
-        executeTaskForTest(newTask("app4", 30));
+        mModelHelper.executeTaskForTest(newTask("app4", 30));
 
         verifyProgressUpdate(30, 8, 9);
     }
 
     private void verifyProgressUpdate(int progress, Integer... idsUpdated) {
         HashSet<Integer> updates = new HashSet<>(Arrays.asList(idsUpdated));
-        for (ItemInfo info : bgDataModel.itemsIdMap) {
+        for (ItemInfo info : mModelHelper.getBgDataModel().itemsIdMap) {
             if (info instanceof WorkspaceItemInfo) {
                 assertEquals(updates.contains(info.id) ? progress: 0,
                         ((WorkspaceItemInfo) info).getInstallProgress());
diff --git a/robolectric_tests/src/com/android/launcher3/popup/PopupPopulatorTest.java b/robolectric_tests/src/com/android/launcher3/popup/PopupPopulatorTest.java
index 83bf7da..7612ae1 100644
--- a/robolectric_tests/src/com/android/launcher3/popup/PopupPopulatorTest.java
+++ b/robolectric_tests/src/com/android/launcher3/popup/PopupPopulatorTest.java
@@ -27,9 +27,10 @@
 
 import android.content.pm.ShortcutInfo;
 
+import com.android.launcher3.util.LauncherRoboTestRunner;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 
 import java.util.ArrayList;
@@ -39,7 +40,7 @@
 /**
  * Tests the sorting and filtering of shortcuts in {@link PopupPopulator}.
  */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(LauncherRoboTestRunner.class)
 public class PopupPopulatorTest {
 
     @Test
diff --git a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
similarity index 78%
rename from tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
rename to robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
index 27990f4..7ef670c 100644
--- a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
+++ b/robolectric_tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -1,3 +1,18 @@
+/*
+ * 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.provider;
 
 import static org.junit.Assert.assertEquals;
@@ -6,21 +21,18 @@
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.MediumTest;
-import androidx.test.runner.AndroidJUnit4;
-
 import com.android.launcher3.LauncherProvider.DatabaseHelper;
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
 
 /**
  * Tests for {@link RestoreDbTask}
  */
-@MediumTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(LauncherRoboTestRunner.class)
 public class RestoreDbTaskTest {
 
     @Test
@@ -83,7 +95,7 @@
         private final long mProfileId;
 
         MyDatabaseHelper(long profileId) {
-            super(InstrumentationRegistry.getContext(), null);
+            super(RuntimeEnvironment.application, null);
             mProfileId = profileId;
         }
 
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowAppWidgetManager.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowAppWidgetManager.java
new file mode 100644
index 0000000..696ffd0
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowAppWidgetManager.java
@@ -0,0 +1,48 @@
+/*
+ * 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.shadows;
+
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.os.Process;
+import android.os.UserHandle;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowAppWidgetManager;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Extension of {@link ShadowAppWidgetManager} with missing shadow methods
+ */
+@Implements(value = AppWidgetManager.class)
+public class LShadowAppWidgetManager extends ShadowAppWidgetManager {
+
+    @Override
+    protected List<AppWidgetProviderInfo> getInstalledProviders() {
+        return getInstalledProvidersForProfile(null);
+    }
+
+    @Implementation
+    public List<AppWidgetProviderInfo> getInstalledProvidersForProfile(UserHandle profile) {
+        UserHandle user = profile == null ? Process.myUserHandle() : profile;
+        return super.getInstalledProviders().stream().filter(
+                info -> user.equals(info.getProfile())).collect(Collectors.toList());
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowBitmap.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowBitmap.java
new file mode 100644
index 0000000..abd90bb
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/LShadowBitmap.java
@@ -0,0 +1,36 @@
+/*
+ * 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.shadows;
+
+import android.graphics.Bitmap;
+import android.graphics.Paint;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowBitmap;
+
+/**
+ * Extension of {@link ShadowBitmap} with missing shadow methods
+ */
+@Implements(value = Bitmap.class)
+public class LShadowBitmap extends ShadowBitmap {
+
+    @Implementation
+    protected Bitmap extractAlpha(Paint paint, int[] offsetXY) {
+        return extractAlpha();
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java b/robolectric_tests/src/com/android/launcher3/shadows/LShadowLauncherApps.java
index 204ec9b..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
@@ -77,7 +78,7 @@
     protected LauncherActivityInfo resolveActivity(Intent intent, UserHandle user) {
         ResolveInfo ri = RuntimeEnvironment.application.getPackageManager()
                 .resolveActivity(intent, 0);
-        return getLauncherActivityInfo(ri.activityInfo);
+        return ri == null ? null : getLauncherActivityInfo(ri.activityInfo);
     }
 
     public LauncherActivityInfo getLauncherActivityInfo(ActivityInfo activityInfo) {
@@ -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/shadows/ShadowLooperExecutor.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java
index d56de3c..a3b7dc7 100644
--- a/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java
@@ -18,25 +18,16 @@
 
 import static com.android.launcher3.util.Executors.createAndStartNewLooper;
 
-import static org.robolectric.shadow.api.Shadow.invokeConstructor;
-import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
+import static org.robolectric.shadow.api.Shadow.directlyOn;
 import static org.robolectric.util.ReflectionHelpers.setField;
 
 import android.os.Handler;
-import android.os.Looper;
 
-import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.LooperExecutor;
 
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
 import org.robolectric.annotation.RealObject;
-import org.robolectric.util.ReflectionHelpers;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Set;
-import java.util.WeakHashMap;
 
 /**
  * Shadow for {@link LooperExecutor} to provide reset functionality for static executors.
@@ -44,25 +35,18 @@
 @Implements(value = LooperExecutor.class, isInAndroidSdk = false)
 public class ShadowLooperExecutor {
 
-    // Keep reference to all created Loopers so they can be torn down after test
-    private static Set<LooperExecutor> executors =
-            Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
-
-    @RealObject private LooperExecutor realExecutor;
+    @RealObject private LooperExecutor mRealExecutor;
 
     @Implementation
-    protected void __constructor__(Looper looper) {
-        invokeConstructor(LooperExecutor.class, realExecutor, from(Looper.class, looper));
-        executors.add(realExecutor);
-    }
-
-    /**
-     * Re-initializes any executor which may have been reset when a test finished
-     */
-    public static void reinitializeStaticExecutors() {
-        for (LooperExecutor executor : new ArrayList<>(executors)) {
-            setField(executor, "mHandler",
-                    new Handler(createAndStartNewLooper(executor.getThread().getName())));
+    protected Handler getHandler() {
+        Handler handler = directlyOn(mRealExecutor, LooperExecutor.class, "getHandler");
+        Thread thread = handler.getLooper().getThread();
+        if (!thread.isAlive()) {
+            // Robolectric destroys all loopers at the end of every test. Since Launcher maintains
+            // some static threads, they need to be reinitialized in case they were destroyed.
+            setField(mRealExecutor, "mHandler",
+                    new Handler(createAndStartNewLooper(thread.getName())));
         }
+        return directlyOn(mRealExecutor, LooperExecutor.class, "getHandler");
     }
 }
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/ShadowMainThreadInitializedObject.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowMainThreadInitializedObject.java
new file mode 100644
index 0000000..6e2ccf8
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowMainThreadInitializedObject.java
@@ -0,0 +1,61 @@
+/*
+ * 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.shadows;
+
+import static org.robolectric.shadow.api.Shadow.invokeConstructor;
+import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
+
+import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.MainThreadInitializedObject.ObjectProvider;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.RealObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+/**
+ * Shadow for {@link MainThreadInitializedObject} to provide reset functionality for static sObjects
+ */
+@Implements(value = MainThreadInitializedObject.class, isInAndroidSdk = false)
+public class ShadowMainThreadInitializedObject {
+
+    // Keep reference to all created MainThreadInitializedObject so they can be cleared after test
+    private static Set<MainThreadInitializedObject> sObjects =
+            Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
+
+    @RealObject private MainThreadInitializedObject mRealObject;
+
+    @Implementation
+    protected void __constructor__(ObjectProvider provider) {
+        invokeConstructor(MainThreadInitializedObject.class, mRealObject,
+                from(ObjectProvider.class, provider));
+        sObjects.add(mRealObject);
+    }
+
+    /**
+     * Resets all the initialized sObjects to be null
+     */
+    public static void resetInitializedObjects() {
+        for (MainThreadInitializedObject object : new ArrayList<>(sObjects)) {
+            object.initializeForTesting(null);
+        }
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/shadows/ShadowTogglableFlag.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowTogglableFlag.java
new file mode 100644
index 0000000..3603dd8
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowTogglableFlag.java
@@ -0,0 +1,38 @@
+/*
+ * 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.shadows;
+
+import android.content.Context;
+
+import com.android.launcher3.uioverrides.TogglableFlag;
+import com.android.launcher3.util.LooperExecutor;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/**
+ * Shadow for {@link LooperExecutor} to provide reset functionality for static executors.
+ */
+@Implements(value = TogglableFlag.class, isInAndroidSdk = false)
+public class ShadowTogglableFlag {
+
+    /**
+     * Mock change listener as it uses internal system classes not available to robolectric
+     */
+    @Implementation
+    protected void addChangeListener(Context context, Runnable r) { }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java b/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java
index aa51ad2..e453e31 100644
--- a/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java
+++ b/robolectric_tests/src/com/android/launcher3/util/GridOccupancyTest.java
@@ -2,7 +2,6 @@
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -11,7 +10,7 @@
 /**
  * Unit tests for {@link GridOccupancy}
  */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(LauncherRoboTestRunner.class)
 public class GridOccupancyTest {
 
     @Test
diff --git a/robolectric_tests/src/com/android/launcher3/util/IntArrayTest.java b/robolectric_tests/src/com/android/launcher3/util/IntArrayTest.java
index c08e198..5974ea5 100644
--- a/robolectric_tests/src/com/android/launcher3/util/IntArrayTest.java
+++ b/robolectric_tests/src/com/android/launcher3/util/IntArrayTest.java
@@ -19,12 +19,11 @@
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
 
 /**
  * Robolectric unit tests for {@link IntArray}
  */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(LauncherRoboTestRunner.class)
 public class IntArrayTest {
 
     @Test
diff --git a/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java b/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java
index 8513353..aedf71e 100644
--- a/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java
+++ b/robolectric_tests/src/com/android/launcher3/util/IntSetTest.java
@@ -20,8 +20,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import org.robolectric.RobolectricTestRunner;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -29,7 +27,7 @@
 /**
  * Robolectric unit tests for {@link IntSet}
  */
-@RunWith(RobolectricTestRunner.class)
+@RunWith(LauncherRoboTestRunner.class)
 public class IntSetTest {
 
     @Test
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
new file mode 100644
index 0000000..655055c
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -0,0 +1,343 @@
+/*
+ * 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.util;
+
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+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;
+import com.android.launcher3.LauncherModel.ModelUpdateTask;
+import com.android.launcher3.LauncherProvider;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.model.AllAppsList;
+import com.android.launcher3.model.BgDataModel;
+
+import org.mockito.ArgumentCaptor;
+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;
+import java.util.concurrent.Executor;
+import java.util.function.Function;
+
+/**
+ * Utility class to help manage Launcher Model and related objects for test.
+ */
+public class LauncherModelHelper {
+
+    public static final int DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+    public static final int HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+
+    public static final int APP_ICON = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+    public static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+    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;
+
+    private BgDataModel mDataModel;
+    private AllAppsList mAllAppsList;
+
+    public LauncherModelHelper() {
+        provider = Robolectric.setupContentProvider(TestLauncherProvider.class);
+        ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, provider);
+    }
+
+    public LauncherModel getModel() {
+        return LauncherAppState.getInstance(RuntimeEnvironment.application).getModel();
+    }
+
+    public synchronized BgDataModel getBgDataModel() {
+        if (mDataModel == null) {
+            mDataModel = ReflectionHelpers.getField(getModel(), "mBgDataModel");
+        }
+        return mDataModel;
+    }
+
+    public synchronized AllAppsList getAllAppsList() {
+        if (mAllAppsList == null) {
+            mAllAppsList = ReflectionHelpers.getField(getModel(), "mBgAllAppsList");
+        }
+        return mAllAppsList;
+    }
+
+    /**
+     * Synchronously executes the task and returns all the UI callbacks posted.
+     */
+    public List<Runnable> executeTaskForTest(ModelUpdateTask task) throws Exception {
+        LauncherModel model = getModel();
+        if (!model.isModelLoaded()) {
+            ReflectionHelpers.setField(model, "mModelLoaded", true);
+        }
+        Executor mockExecutor = mock(Executor.class);
+        model.enqueueModelUpdateTask(new ModelUpdateTask() {
+            @Override
+            public void init(LauncherAppState app, LauncherModel model, BgDataModel dataModel,
+                    AllAppsList allAppsList, Executor uiExecutor) {
+                task.init(app, model, dataModel, allAppsList, mockExecutor);
+            }
+
+            @Override
+            public void run() {
+                task.run();
+            }
+        });
+        MODEL_EXECUTOR.submit(() -> null).get();
+
+        ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
+        verify(mockExecutor, atLeast(0)).execute(captor.capture());
+        return captor.getAllValues();
+    }
+
+    /**
+     * Synchronously executes a task on the model
+     */
+    public <T> T executeSimpleTask(Function<BgDataModel, T> task) throws Exception {
+        BgDataModel dataModel = getBgDataModel();
+        return MODEL_EXECUTOR.submit(() -> task.apply(dataModel)).get();
+    }
+
+    /**
+     * Initializes mock data for the test.
+     */
+    public void initializeData(String resourceName) throws Exception {
+        Context targetContext = RuntimeEnvironment.application;
+        BgDataModel bgDataModel = getBgDataModel();
+        AllAppsList allAppsList = getAllAppsList();
+
+        MODEL_EXECUTOR.submit(() -> {
+            try (BufferedReader reader = new BufferedReader(new InputStreamReader(
+                    this.getClass().getResourceAsStream(resourceName)))) {
+                String line;
+                HashMap<String, Class> classMap = new HashMap<>();
+                while ((line = reader.readLine()) != null) {
+                    line = line.trim();
+                    if (line.startsWith("#") || line.isEmpty()) {
+                        continue;
+                    }
+                    String[] commands = line.split(" ");
+                    switch (commands[0]) {
+                        case "classMap":
+                            classMap.put(commands[1], Class.forName(commands[2]));
+                            break;
+                        case "bgItem":
+                            bgDataModel.addItem(targetContext,
+                                    (ItemInfo) initItem(classMap.get(commands[1]), commands, 2),
+                                    false);
+                            break;
+                        case "allApps":
+                            allAppsList.add((AppInfo) initItem(AppInfo.class, commands, 1), null);
+                            break;
+                    }
+                }
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }).get();
+    }
+
+    private Object initItem(Class clazz, String[] fieldDef, int startIndex) throws Exception {
+        HashMap<String, Field> cache = mFieldCache.get(clazz);
+        if (cache == null) {
+            cache = new HashMap<>();
+            Class c = clazz;
+            while (c != null) {
+                for (Field f : c.getDeclaredFields()) {
+                    f.setAccessible(true);
+                    cache.put(f.getName(), f);
+                }
+                c = c.getSuperclass();
+            }
+            mFieldCache.put(clazz, cache);
+        }
+
+        Object item = clazz.newInstance();
+        for (int i = startIndex; i < fieldDef.length; i++) {
+            String[] fieldData = fieldDef[i].split("=", 2);
+            Field f = cache.get(fieldData[0]);
+            Class type = f.getType();
+            if (type == int.class || type == long.class) {
+                f.set(item, Integer.parseInt(fieldData[1]));
+            } else if (type == CharSequence.class || type == String.class) {
+                f.set(item, fieldData[1]);
+            } else if (type == Intent.class) {
+                if (!fieldData[1].startsWith("#Intent")) {
+                    fieldData[1] = "#Intent;" + fieldData[1] + ";end";
+                }
+                f.set(item, Intent.parseUri(fieldData[1], 0));
+            } else if (type == ComponentName.class) {
+                f.set(item, ComponentName.unflattenFromString(fieldData[1]));
+            } else {
+                throw new Exception("Added parsing logic for "
+                        + f.getName() + " of type " + f.getType());
+            }
+        }
+        return item;
+    }
+
+    /**
+     * Adds a dummy item in the DB.
+     * @param type {@link #APP_ICON} or {@link #SHORTCUT} or >= 2 for
+     *             folder (where the type represents the number of items in the folder).
+     */
+    public int addItem(int type, int screen, int container, int x, int y) {
+        Context context = RuntimeEnvironment.application;
+        int id = LauncherSettings.Settings.call(context.getContentResolver(),
+                LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
+                .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+
+        ContentValues values = new ContentValues();
+        values.put(LauncherSettings.Favorites._ID, id);
+        values.put(LauncherSettings.Favorites.CONTAINER, container);
+        values.put(LauncherSettings.Favorites.SCREEN, screen);
+        values.put(LauncherSettings.Favorites.CELLX, x);
+        values.put(LauncherSettings.Favorites.CELLY, y);
+        values.put(LauncherSettings.Favorites.SPANX, 1);
+        values.put(LauncherSettings.Favorites.SPANY, 1);
+
+        if (type == APP_ICON || type == SHORTCUT) {
+            values.put(LauncherSettings.Favorites.ITEM_TYPE, type);
+            values.put(LauncherSettings.Favorites.INTENT,
+                    new Intent(Intent.ACTION_MAIN).setPackage(TEST_PACKAGE).toUri(0));
+        } else {
+            values.put(LauncherSettings.Favorites.ITEM_TYPE,
+                    LauncherSettings.Favorites.ITEM_TYPE_FOLDER);
+            // Add folder items.
+            for (int i = 0; i < type; i++) {
+                addItem(APP_ICON, 0, id, 0, 0);
+            }
+        }
+
+        context.getContentResolver().insert(LauncherSettings.Favorites.CONTENT_URI, values);
+        return id;
+    }
+
+    public int[][][] createGrid(int[][][] typeArray) {
+        return createGrid(typeArray, 1);
+    }
+
+    /**
+     * Initializes the DB with dummy elements to represent the provided grid structure.
+     * @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for
+     *                  type definitions. The first dimension represents the screens and the next
+     *                  two represent the workspace grid.
+     * @param startScreen First screen id from where the icons will be added.
+     * @return the same grid representation where each entry is the corresponding item id.
+     */
+    public int[][][] createGrid(int[][][] typeArray, int startScreen) {
+        Context context = RuntimeEnvironment.application;
+        LauncherSettings.Settings.call(context.getContentResolver(),
+                LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
+        int[][][] ids = new int[typeArray.length][][];
+
+        for (int i = 0; i < typeArray.length; i++) {
+            // Add screen to DB
+            int screenId = startScreen + i;
+
+            // Keep the screen id counter up to date
+            LauncherSettings.Settings.call(context.getContentResolver(),
+                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
+
+            ids[i] = new int[typeArray[i].length][];
+            for (int y = 0; y < typeArray[i].length; y++) {
+                ids[i][y] = new int[typeArray[i][y].length];
+                for (int x = 0; x < typeArray[i][y].length; x++) {
+                    if (typeArray[i][y][x] < 0) {
+                        // Empty cell
+                        ids[i][y][x] = -1;
+                    } else {
+                        ids[i][y][x] = addItem(typeArray[i][y][x], screenId, DESKTOP, x, y);
+                    }
+                }
+            }
+        }
+
+        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/robolectric_tests/src/com/android/launcher3/util/LauncherRoboTestRunner.java b/robolectric_tests/src/com/android/launcher3/util/LauncherRoboTestRunner.java
new file mode 100644
index 0000000..5c6b486
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherRoboTestRunner.java
@@ -0,0 +1,90 @@
+/*
+ * 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.util;
+
+import static org.mockito.Mockito.mock;
+
+import com.android.launcher3.shadows.LShadowAppWidgetManager;
+import com.android.launcher3.shadows.LShadowBitmap;
+import com.android.launcher3.shadows.LShadowLauncherApps;
+import com.android.launcher3.shadows.LShadowUserManager;
+import com.android.launcher3.shadows.ShadowLooperExecutor;
+import com.android.launcher3.shadows.ShadowMainThreadInitializedObject;
+import com.android.launcher3.shadows.ShadowTogglableFlag;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+
+import org.junit.runners.model.InitializationError;
+import org.robolectric.DefaultTestLifecycle;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.TestLifecycle;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLog;
+
+import java.lang.reflect.Method;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Test runner with Launcher specific configurations
+ */
+public class LauncherRoboTestRunner extends RobolectricTestRunner {
+
+    private static final Class<?>[] SHADOWS = new Class<?>[] {
+            LShadowAppWidgetManager.class,
+            LShadowUserManager.class,
+            LShadowLauncherApps.class,
+            LShadowBitmap.class,
+
+            ShadowLooperExecutor.class,
+            ShadowMainThreadInitializedObject.class,
+            ShadowTogglableFlag.class,
+    };
+
+    public LauncherRoboTestRunner(Class<?> testClass) throws InitializationError {
+        super(testClass);
+    }
+
+    @Override
+    protected Config buildGlobalConfig() {
+        return new Config.Builder().setShadows(SHADOWS).build();
+    }
+
+    @Nonnull
+    @Override
+    protected Class<? extends TestLifecycle> getTestLifecycleClass() {
+        return LauncherTestLifecycle.class;
+    }
+
+    public static class LauncherTestLifecycle extends DefaultTestLifecycle {
+
+        @Override
+        public void beforeTest(Method method) {
+            super.beforeTest(method);
+            ShadowLog.stream = System.out;
+
+            // Disable plugins
+            PluginManagerWrapper.INSTANCE.initializeForTesting(mock(PluginManagerWrapper.class));
+        }
+
+        @Override
+        public void afterTest(Method method) {
+            super.afterTest(method);
+
+            ShadowLog.stream = null;
+            ShadowMainThreadInitializedObject.resetInitializedObjects();
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
similarity index 83%
rename from tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
rename to robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
index 57b0b09..daae818 100644
--- a/tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
@@ -19,16 +19,15 @@
 import static org.mockito.Matchers.isNull;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.robolectric.Shadows.shadowOf;
 
 import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.view.LayoutInflater;
 
 import androidx.recyclerview.widget.RecyclerView;
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
@@ -37,19 +36,21 @@
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.util.MultiHashMap;
+import com.android.launcher3.util.LauncherRoboTestRunner;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
 
 import java.util.ArrayList;
-import java.util.Map;
+import java.util.Collections;
 
-@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(LauncherRoboTestRunner.class)
 public class WidgetsListAdapterTest {
 
     @Mock private LayoutInflater mMockLayoutInflater;
@@ -64,7 +65,7 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mContext = InstrumentationRegistry.getTargetContext();
+        mContext = RuntimeEnvironment.application;
         mTestProfile = new InvariantDeviceProfile();
         mTestProfile.numRows = 5;
         mTestProfile.numColumns = 5;
@@ -121,15 +122,19 @@
     /**
      * Helper method to generate the sample widget model map that can be used for the tests
      * @param num the number of WidgetItem the map should contain
-     * @return
      */
     private ArrayList<WidgetListRowEntry> generateSampleMap(int num) {
         ArrayList<WidgetListRowEntry> result = new ArrayList<>();
         if (num <= 0) return result;
+        ShadowPackageManager spm = shadowOf(mContext.getPackageManager());
 
-        MultiHashMap<PackageItemInfo, WidgetItem> newMap = new MultiHashMap();
-        WidgetManagerHelper widgetManager = new WidgetManagerHelper(mContext);
-        for (AppWidgetProviderInfo widgetInfo : widgetManager.getAllProviders(null)) {
+        for (int i = 0; i < num; i++) {
+            ComponentName cn = new ComponentName("com.dummy.apk" + i, "DummyWidet");
+
+            AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+            widgetInfo.provider = cn;
+            ReflectionHelpers.setField(widgetInfo, "providerInfo", spm.addReceiverIfNotPresent(cn));
+
             WidgetItem wi = new WidgetItem(LauncherAppWidgetProviderInfo
                     .fromProviderInfo(mContext, widgetInfo), mTestProfile, mIconCache);
 
@@ -137,13 +142,8 @@
             pInfo.title = pInfo.packageName;
             pInfo.user = wi.user;
             pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
-            newMap.addToList(pInfo, wi);
-            if (newMap.size() == num) {
-                break;
-            }
-        }
-        for (Map.Entry<PackageItemInfo, ArrayList<WidgetItem>> entry : newMap.entrySet()) {
-            result.add(new WidgetListRowEntry(entry.getKey(), entry.getValue()));
+
+            result.add(new WidgetListRowEntry(pInfo, new ArrayList<>(Collections.singleton(wi))));
         }
 
         return result;
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/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index f319ae1..ea63fa7 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -24,7 +25,12 @@
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
+import android.content.pm.LauncherApps;
 import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Log;
 import android.view.ContextThemeWrapper;
 
 import androidx.annotation.IntDef;
@@ -47,6 +53,8 @@
 public abstract class BaseActivity extends Activity
         implements UserEventDelegate, LogStateProvider, ActivityContext {
 
+    private static final String TAG = "BaseActivity";
+
     public static final int INVISIBLE_BY_STATE_HANDLER = 1 << 0;
     public static final int INVISIBLE_BY_APP_TRANSITIONS = 1 << 1;
     public static final int INVISIBLE_BY_PENDING_FLAGS = 1 << 2;
@@ -312,6 +320,22 @@
         writer.println(prefix + "mForceInvisible: " + mForceInvisible);
     }
 
+    /**
+     * A wrapper around the platform method with Launcher specific checks
+     */
+    public void startShortcut(String packageName, String id, Rect sourceBounds,
+            Bundle startActivityOptions, UserHandle user) {
+        if (GO_DISABLE_WIDGETS) {
+            return;
+        }
+        try {
+            getSystemService(LauncherApps.class).startShortcut(packageName, id, sourceBounds,
+                    startActivityOptions, user);
+        } catch (SecurityException | IllegalStateException e) {
+            Log.e(TAG, "Failed to start shortcut", e);
+        }
+    }
+
     public static <T extends BaseActivity> T fromContext(Context context) {
         if (context instanceof BaseActivity) {
             return (T) context;
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index 893f64a..df15fc1 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -35,7 +35,6 @@
 
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.model.AppLaunchTracker;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.uioverrides.DisplayRotationListener;
 import com.android.launcher3.uioverrides.WallpaperColorInfo;
 import com.android.launcher3.util.PackageManagerHelper;
@@ -174,8 +173,8 @@
                 AppLaunchTracker.INSTANCE.get(this).onStartApp(intent.getComponent(), user,
                         sourceContainer);
             }
-            getUserEventDispatcher().logAppLaunch(v, intent);
-            getStatsLogManager().logAppLaunch(v, intent);
+            getUserEventDispatcher().logAppLaunch(v, intent, user);
+            getStatsLogManager().logAppLaunch(v, intent, user);
             return true;
         } catch (NullPointerException|ActivityNotFoundException|SecurityException e) {
             Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
@@ -198,8 +197,7 @@
                 if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
                     String id = ((WorkspaceItemInfo) info).getDeepShortcutId();
                     String packageName = intent.getPackage();
-                    DeepShortcutManager.getInstance(this).startShortcut(
-                            packageName, id, intent.getSourceBounds(), optsBundle, info.user);
+                    startShortcut(packageName, id, intent.getSourceBounds(), optsBundle, info.user);
                     AppLaunchTracker.INSTANCE.get(this).onStartShortcut(packageName, id, info.user,
                             sourceContainer);
                 } else {
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 52a393f..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;
@@ -108,6 +108,7 @@
     @Override
     public void onCommitCompletion(CompletionInfo text) {
         setText(text.getText());
+        setSelection(text.getText().length());
     }
 
     /**
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/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
index df03027..3eb02b3 100644
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -49,8 +49,8 @@
 import com.android.launcher3.icons.GraphicsUtils;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.Thunk;
@@ -538,12 +538,9 @@
                     return new PendingInstallShortcutInfo(info, context);
                 }
             } else if (decoder.optBoolean(DEEPSHORTCUT_TYPE_KEY)) {
-                DeepShortcutManager sm = DeepShortcutManager.getInstance(context);
-                List<ShortcutInfo> si = sm.queryForFullDetails(
-                        decoder.launcherIntent.getPackage(),
-                        Arrays.asList(decoder.launcherIntent.getStringExtra(
-                                ShortcutKey.EXTRA_SHORTCUT_ID)),
-                        decoder.user);
+                List<ShortcutInfo> si = ShortcutKey.fromIntent(decoder.launcherIntent, decoder.user)
+                        .buildRequest(context)
+                        .query(ShortcutRequest.ALL);
                 if (si.isEmpty()) {
                     return null;
                 } else {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 7265a1a..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;
     }
@@ -1429,6 +1421,9 @@
 
     @Override
     protected void onNewIntent(Intent intent) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            Log.d(TestProtocol.PERMANENT_DIAG_TAG, "Launcher.onNewIntent: " + intent);
+        }
         Object traceToken = TraceHelper.INSTANCE.beginSection(ON_NEW_INTENT_EVT);
         super.onNewIntent(intent);
 
@@ -1536,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 {
@@ -1737,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);
@@ -1950,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;
@@ -2098,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;
@@ -2332,6 +2331,8 @@
 
     public void onPageBoundSynchronously(int page) {
         mSynchronouslyBoundPage = page;
+        mWorkspace.setCurrentPage(page);
+        mPageToBindSynchronously = PagedView.INVALID_PAGE;
     }
 
     @Override
@@ -2396,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 67fd7db..cf978b5 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.config.FeatureFlags.IS_DOGFOOD_BUILD;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
 
 import android.content.Context;
 import android.content.Intent;
@@ -54,19 +55,19 @@
 import com.android.launcher3.pm.InstallSessionTracker;
 import com.android.launcher3.pm.PackageInstallInfo;
 import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
+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;
@@ -81,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
@@ -98,7 +100,7 @@
         }
     }
 
-    @Thunk WeakReference<Callbacks> mCallbacks;
+    private final ArrayList<Callbacks> mCallbacksList = new ArrayList<>(1);
 
     // < only access in worker thread >
     private final AllAppsList mBgAllAppsList;
@@ -107,18 +109,15 @@
      * All the static data should be accessed on the background thread, A lock should be acquired
      * on this object when accessing any data from this model.
      */
-    static final BgDataModel sBgDataModel = new BgDataModel();
+    private final BgDataModel mBgDataModel = new BgDataModel();
 
     // Runnable to check if the shortcuts permission has changed.
     private final Runnable mShortcutPermissionCheckRunnable = new Runnable() {
         @Override
         public void run() {
-            if (mModelLoaded) {
-                boolean hasShortcutHostPermission =
-                        DeepShortcutManager.getInstance(mApp.getContext()).hasHostPermission();
-                if (hasShortcutHostPermission != sBgDataModel.hasShortcutHostPermission) {
-                    forceReload();
-                }
+            if (mModelLoaded && hasShortcutsPermission(mApp.getContext())
+                    != mBgDataModel.hasShortcutHostPermission) {
+                forceReload();
             }
         }
     };
@@ -129,31 +128,30 @@
     }
 
     /**
+     * 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));
     }
 
     public ModelWriter getWriter(boolean hasVerticalHotseat, boolean verifyChanges) {
-        return new ModelWriter(mApp.getContext(), this, sBgDataModel,
+        return new ModelWriter(mApp.getContext(), this, mBgDataModel,
                 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;
@@ -220,8 +218,8 @@
         Context context = mApp.getContext();
         onPackageChanged(packageName, user);
 
-        List<ShortcutInfo> pinnedShortcuts = DeepShortcutManager.getInstance(context)
-                .queryForPinnedShortcuts(packageName, user);
+        List<ShortcutInfo> pinnedShortcuts = new ShortcutRequest(context, user)
+                .forPackage(packageName).query(ShortcutRequest.PINNED);
         if (!pinnedShortcuts.isEmpty()) {
             enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, pinnedShortcuts, user,
                     false));
@@ -253,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();
@@ -276,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, sBgDataModel,
-                        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).
@@ -327,21 +363,24 @@
 
     /**
      * 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;
         }
     }
 
     public void startLoaderForResults(LoaderResults results) {
         synchronized (mLock) {
             stopLoader();
-            mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, sBgDataModel, results);
+            mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, mBgDataModel, results);
 
             // Always post the loader task, instead of running directly (even on same thread) so
             // that we exit any nested synchronized blocks
@@ -489,7 +528,7 @@
     }
 
     public void enqueueModelUpdateTask(ModelUpdateTask task) {
-        task.init(mApp, this, sBgDataModel, mBgAllAppsList, MAIN_EXECUTOR);
+        task.init(mApp, this, mBgDataModel, mBgAllAppsList, mMainExecutor);
         MODEL_EXECUTOR.execute(task);
     }
 
@@ -560,10 +599,24 @@
                         + " componentName=" + info.componentName.getPackageName());
             }
         }
-        sBgDataModel.dump(prefix, fd, writer, args);
+        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/ResourceUtils.java b/src/com/android/launcher3/ResourceUtils.java
index 73e705b..7f327a5 100644
--- a/src/com/android/launcher3/ResourceUtils.java
+++ b/src/com/android/launcher3/ResourceUtils.java
@@ -29,7 +29,7 @@
         return getDimenByName(resName, res, 48);
     }
 
-    private static int getDimenByName(String resName, Resources res, int defaultValue) {
+    public static int getDimenByName(String resName, Resources res, int defaultValue) {
         final int frameSize;
         final int frameSizeResID = res.getIdentifier(resName, "dimen", "android");
         if (frameSizeResID != 0) {
@@ -40,6 +40,17 @@
         return frameSize;
     }
 
+    public static boolean getBoolByName(String resName, Resources res, boolean defaultValue) {
+        final boolean val;
+        final int resId = res.getIdentifier(resName, "bool", "android");
+        if (resId != 0) {
+            val = res.getBoolean(resId);
+        } else {
+            val = defaultValue;
+        }
+        return val;
+    }
+
     public static int pxFromDp(float size, DisplayMetrics metrics) {
         return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, metrics));
     }
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 2bec0ba..af9a1b4 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -65,9 +65,10 @@
 import com.android.launcher3.graphics.TintedDrawableSpan;
 import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.icons.ShortcutCachingLogic;
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.views.Transposable;
@@ -540,15 +541,14 @@
                 outObj[0] = activityInfo;
                 return activityInfo.getFullResIcon(appState.getIconCache());
             }
-            ShortcutKey key = ShortcutKey.fromItemInfo(info);
-            DeepShortcutManager sm = DeepShortcutManager.getInstance(launcher);
-            List<ShortcutInfo> si = sm.queryForFullDetails(
-                    key.componentName.getPackageName(), Arrays.asList(key.getId()), key.user);
+            List<ShortcutInfo> si = ShortcutKey.fromItemInfo(info)
+                    .buildRequest(launcher)
+                    .query(ShortcutRequest.ALL);
             if (si.isEmpty()) {
                 return null;
             } else {
                 outObj[0] = si.get(0);
-                return sm.getShortcutIconDrawable(si.get(0),
+                return ShortcutCachingLogic.getIcon(launcher, si.get(0),
                         appState.getInvariantDeviceProfile().fillResIconDpi);
             }
         } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
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/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 0c4be62..10e2821 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -15,13 +15,14 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
+
 import android.content.Context;
 import android.content.pm.PackageManager;
 
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LabelComparator;
@@ -398,7 +399,7 @@
 
     private boolean shouldShowWorkFooter() {
         return mIsWork && Utilities.ATLEAST_P &&
-                (DeepShortcutManager.getInstance(mLauncher).hasHostPermission()
+                (hasShortcutsPermission(mLauncher)
                         || mLauncher.checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
                         == PackageManager.PERMISSION_GRANTED);
     }
diff --git a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
index d47a40e..28579c1 100644
--- a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
+++ b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
@@ -57,6 +58,7 @@
         parcel.putInt(TestProtocol.STATE_FIELD, stateOrdinal);
 
         sendEventToTest(accessibilityManager, TestProtocol.SWITCHED_TO_STATE_MESSAGE, parcel);
+        Log.d(TestProtocol.PERMANENT_DIAG_TAG, "sendStateEventToTest: " + stateOrdinal);
     }
 
     public static void sendScrollFinishedEventToTest(Context context) {
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 80c7056..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,
@@ -95,7 +98,7 @@
             "ENABLE_QUICKSTEP_LIVE_TILE", false, "Enable live tile in Quickstep overview");
 
     public static final TogglableFlag ENABLE_HINTS_IN_OVERVIEW = new TogglableFlag(
-            "ENABLE_HINTS_IN_OVERVIEW", true,
+            "ENABLE_HINTS_IN_OVERVIEW", false,
             "Show chip hints and gleams on the overview screen");
 
     public static final TogglableFlag FAKE_LANDSCAPE_UI = new TogglableFlag(
@@ -114,7 +117,7 @@
             "ENABLE_PREDICTION_DISMISS", false, "Allow option to dimiss apps from predicted list");
 
     public static final TogglableFlag ENABLE_QUICK_CAPTURE_GESTURE = new TogglableFlag(
-            "ENABLE_QUICK_CAPTURE_GESTURE", false, "Swipe from right to left to quick capture");
+            "ENABLE_QUICK_CAPTURE_GESTURE", true, "Swipe from right to left to quick capture");
 
     public static final TogglableFlag ASSISTANT_GIVES_LAUNCHER_FOCUS = new TogglableFlag(
             "ASSISTANT_GIVES_LAUNCHER_FOCUS", false,
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index b72fd98..8adec27 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_DISCOVERY_BOUNCE;
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.Utilities.ATLEAST_Q;
 
 import android.animation.ValueAnimator;
 import android.content.ComponentName;
@@ -56,6 +57,12 @@
 public class DragController implements DragDriver.EventListener, TouchController {
     private static final boolean PROFILE_DRAWING_DURING_DRAG = false;
 
+    /**
+     * When a drag is started from a deep press, you need to drag this much farther than normal to
+     * end a pre-drag. See {@link DragOptions.PreDragCondition#shouldStartDrag(double)}.
+     */
+    private static final int DEEP_PRESS_DISTANCE_FACTOR = 3;
+
     @Thunk Launcher mLauncher;
     private FlingToDeleteHelper mFlingToDeleteHelper;
 
@@ -91,9 +98,10 @@
 
     private DropTarget mLastDropTarget;
 
-    @Thunk int mLastTouch[] = new int[2];
-    @Thunk long mLastTouchUpTime = -1;
-    @Thunk int mDistanceSinceScroll = 0;
+    private final int[] mLastTouch = new int[2];
+    private long mLastTouchUpTime = -1;
+    private int mLastTouchClassification;
+    private int mDistanceSinceScroll = 0;
 
     private int mTmpPoint[] = new int[2];
     private Rect mDragLayerRect = new Rect();
@@ -204,7 +212,7 @@
         }
 
         mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
-        dragView.show(mMotionDownX, mMotionDownY);
+        dragView.show(mLastTouch[0], mLastTouch[1]);
         mDistanceSinceScroll = 0;
 
         if (!mIsInPreDrag) {
@@ -213,9 +221,7 @@
             mOptions.preDragCondition.onPreDragStart(mDragObject);
         }
 
-        mLastTouch[0] = mMotionDownX;
-        mLastTouch[1] = mMotionDownY;
-        handleMoveEvent(mMotionDownX, mMotionDownY);
+        handleMoveEvent(mLastTouch[0], mLastTouch[1]);
         mLauncher.getUserEventDispatcher().resetActionDurationMillis();
         return dragView;
     }
@@ -430,6 +436,11 @@
         final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
         final int dragLayerX = dragLayerPos[0];
         final int dragLayerY = dragLayerPos[1];
+        mLastTouch[0] = dragLayerX;
+        mLastTouch[1] = dragLayerY;
+        if (ATLEAST_Q) {
+            mLastTouchClassification = ev.getClassification();
+        }
 
         switch (action) {
             case MotionEvent.ACTION_DOWN:
@@ -488,8 +499,12 @@
         mLastTouch[0] = x;
         mLastTouch[1] = y;
 
+        int distanceDragged = mDistanceSinceScroll;
+        if (ATLEAST_Q && mLastTouchClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS) {
+            distanceDragged /= DEEP_PRESS_DISTANCE_FACTOR;
+        }
         if (mIsInPreDrag && mOptions.preDragCondition != null
-                && mOptions.preDragCondition.shouldStartDrag(mDistanceSinceScroll)) {
+                && mOptions.preDragCondition.shouldStartDrag(distanceDragged)) {
             callOnDragStart();
         }
     }
@@ -579,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/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index 4d3599e..e67d244 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -35,7 +35,6 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.model.PackageItemInfo;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.util.Themes;
 
 import java.util.function.Supplier;
@@ -133,8 +132,8 @@
 
     public BitmapInfo createShortcutIconLegacy(ShortcutInfo shortcutInfo, boolean badged,
             @Nullable Supplier<ItemInfoWithIcon> fallbackIconProvider) {
-        Drawable unbadgedDrawable = DeepShortcutManager.getInstance(mContext)
-                .getShortcutIconDrawable(shortcutInfo, mFillResIconDpi);
+        Drawable unbadgedDrawable = ShortcutCachingLogic.getIcon(
+                mContext, shortcutInfo, mFillResIconDpi);
         IconCache cache = LauncherAppState.getInstance(mContext).getIconCache();
         final Bitmap unbadgedBitmap;
         if (unbadgedDrawable != null) {
diff --git a/src/com/android/launcher3/icons/ShortcutCachingLogic.java b/src/com/android/launcher3/icons/ShortcutCachingLogic.java
index 5c21470..b856dd1 100644
--- a/src/com/android/launcher3/icons/ShortcutCachingLogic.java
+++ b/src/com/android/launcher3/icons/ShortcutCachingLogic.java
@@ -16,19 +16,22 @@
 
 package com.android.launcher3.icons;
 
+import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
+
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.LauncherApps;
 import android.content.pm.PackageInfo;
 import android.content.pm.ShortcutInfo;
 import android.graphics.drawable.Drawable;
 import android.os.UserHandle;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.cache.CachingLogic;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.Themes;
 
@@ -37,6 +40,8 @@
  */
 public class ShortcutCachingLogic implements CachingLogic<ShortcutInfo> {
 
+    private static final String TAG = "ShortcutCachingLogic";
+
     @Override
     public ComponentName getComponent(ShortcutInfo info) {
         return ShortcutKey.fromInfo(info).componentName;
@@ -56,8 +61,8 @@
     @Override
     public BitmapInfo loadIcon(Context context, ShortcutInfo info) {
         try (LauncherIcons li = LauncherIcons.obtain(context)) {
-            Drawable unbadgedDrawable = DeepShortcutManager.getInstance(context)
-                    .getShortcutIconDrawable(info, LauncherAppState.getIDP(context).fillResIconDpi);
+            Drawable unbadgedDrawable = ShortcutCachingLogic.getIcon(
+                    context, info, LauncherAppState.getIDP(context).fillResIconDpi);
             if (unbadgedDrawable == null) return BitmapInfo.LOW_RES_INFO;
             return new BitmapInfo(li.createScaledBitmapWithoutShadow(
                     unbadgedDrawable, 0), Themes.getColorAccent(context));
@@ -76,4 +81,21 @@
     public boolean addToMemCache() {
         return false;
     }
+
+    /**
+     * Similar to {@link LauncherApps#getShortcutIconDrawable(ShortcutInfo, int)} with additional
+     * Launcher specific checks
+     */
+    public static Drawable getIcon(Context context, ShortcutInfo shortcutInfo, int density) {
+        if (GO_DISABLE_WIDGETS) {
+            return null;
+        }
+        try {
+            return context.getSystemService(LauncherApps.class)
+                    .getShortcutIconDrawable(shortcutInfo, density);
+        } catch (SecurityException | IllegalStateException e) {
+            Log.e(TAG, "Failed to get shortcut icon", e);
+            return null;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/logging/DumpTargetWrapper.java b/src/com/android/launcher3/logging/DumpTargetWrapper.java
index 365e8f2..067bdfd 100644
--- a/src/com/android/launcher3/logging/DumpTargetWrapper.java
+++ b/src/com/android/launcher3/logging/DumpTargetWrapper.java
@@ -15,17 +15,22 @@
  */
 package com.android.launcher3.logging;
 
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+
+import android.content.ComponentName;
 import android.os.Process;
 import android.text.TextUtils;
 
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.model.nano.LauncherDumpProto;
 import com.android.launcher3.model.nano.LauncherDumpProto.ContainerType;
 import com.android.launcher3.model.nano.LauncherDumpProto.DumpTarget;
 import com.android.launcher3.model.nano.LauncherDumpProto.ItemType;
 import com.android.launcher3.model.nano.LauncherDumpProto.UserType;
+import com.android.launcher3.util.ShortcutUtil;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -73,20 +78,23 @@
     public DumpTarget newItemTarget(ItemInfo info) {
         DumpTarget dt = new DumpTarget();
         dt.type = DumpTarget.Type.ITEM;
-
+        if (info == null) {
+            return dt;
+        }
         switch (info.itemType) {
             case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                 dt.itemType = ItemType.APP_ICON;
                 break;
-            case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
-                dt.itemType = ItemType.UNKNOWN_ITEMTYPE;
-                break;
             case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
                 dt.itemType = ItemType.WIDGET;
                 break;
-            case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+            case ITEM_TYPE_DEEP_SHORTCUT:
+            case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                 dt.itemType = ItemType.SHORTCUT;
                 break;
+            default:
+                dt.itemType = ItemType.UNKNOWN_ITEMTYPE;
+                break;
         }
         return dt;
     }
@@ -120,6 +128,9 @@
     }
 
     private static String getItemStr(DumpTarget t) {
+        if (t == null) {
+            return "";
+        }
         String typeStr = LoggerUtils.getFieldName(t.itemType, ItemType.class);
         if (!TextUtils.isEmpty(t.packageName)) {
             typeStr += ", package=" + t.packageName;
@@ -132,8 +143,15 @@
     }
 
     public DumpTarget writeToDumpTarget(ItemInfo info) {
-        node.component = info.getTargetComponent() == null? "":
-                info.getTargetComponent().flattenToString();
+        if (info == null) {
+            return node;
+        }
+        if (ShortcutUtil.isDeepShortcut(info)) {
+            node.component = ((WorkspaceItemInfo) info).getDeepShortcutId();
+        } else {
+            ComponentName cmp = info.getTargetComponent();
+            node.component = cmp == null ? "" : cmp.flattenToString();
+        }
         node.packageName = info.getTargetComponent() == null? "":
                 info.getTargetComponent().getPackageName();
         if (info instanceof LauncherAppWidgetInfo) {
diff --git a/src/com/android/launcher3/logging/FileLog.java b/src/com/android/launcher3/logging/FileLog.java
index 04cf20a..2c972a0 100644
--- a/src/com/android/launcher3/logging/FileLog.java
+++ b/src/com/android/launcher3/logging/FileLog.java
@@ -42,6 +42,8 @@
     private static Handler sHandler = null;
     private static File sLogsDirectory = null;
 
+    private static final int LOG_DAYS = 2;
+
     public static void setDir(File logsDir) {
         if (ENABLED) {
             synchronized (DATE_FORMAT) {
@@ -147,7 +149,7 @@
                 case MSG_WRITE: {
                     Calendar cal = Calendar.getInstance();
                     // suffix with 0 or 1 based on the day of the year.
-                    String fileName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) & 1);
+                    String fileName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) % LOG_DAYS);
 
                     if (!fileName.equals(mCurrentFileName)) {
                         closeWriter();
@@ -195,8 +197,9 @@
                             (Pair<PrintWriter, CountDownLatch>) msg.obj;
 
                     if (p.first != null) {
-                        dumpFile(p.first, FILE_NAME_PREFIX + 0);
-                        dumpFile(p.first, FILE_NAME_PREFIX + 1);
+                        for (int i = 0; i < LOG_DAYS; i++) {
+                            dumpFile(p.first, FILE_NAME_PREFIX + i);
+                        }
                     }
                     p.second.countDown();
                     return true;
@@ -226,4 +229,15 @@
             }
         }
     }
+
+    /**
+     * Gets files used for FileLog
+     */
+    public static File[] getLogFiles() {
+        File[] files = new File[LOG_DAYS];
+        for (int i = 0; i < LOG_DAYS; i++) {
+            files[i] = new File(sLogsDirectory, FILE_NAME_PREFIX + i);
+        }
+        return files;
+    }
 }
diff --git a/src/com/android/launcher3/logging/LoggerUtils.java b/src/com/android/launcher3/logging/LoggerUtils.java
index 598792a..f352b46 100644
--- a/src/com/android/launcher3/logging/LoggerUtils.java
+++ b/src/com/android/launcher3/logging/LoggerUtils.java
@@ -142,8 +142,10 @@
             typeStr += ", grid(" + t.gridX + "," + t.gridY + ")";
         } else if ((t.packageNameHash != 0 || t.componentHash != 0 || t.intentHash != 0)
                 && t.itemType != ItemType.TASK) {
-            typeStr += ", predictiveRank=" + t.predictedRank + ", grid(" + t.gridX + "," + t.gridY
-                    + "), span(" + t.spanX + "," + t.spanY + "), pageIdx=" + t.pageIndex;
+            typeStr +=
+                    ", isWorkApp=" + t.isWorkApp + ", predictiveRank=" + t.predictedRank + ", grid("
+                            + t.gridX + "," + t.gridY + "), span(" + t.spanX + "," + t.spanY
+                            + "), pageIdx=" + t.pageIndex;
         }
         if (t.searchQueryLength != 0) {
             typeStr += ", searchQueryLength=" + t.searchQueryLength;
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index cad95b0..9dfd7ab 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -17,12 +17,15 @@
 
 import android.content.Context;
 import android.content.Intent;
+import android.os.UserHandle;
 import android.view.View;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.R;
+import com.android.launcher3.logging.StatsLogUtils.LogStateProvider;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ResourceBasedOverride;
-import com.android.launcher3.logging.StatsLogUtils.LogStateProvider;
 
 /**
  * Handles the user event logging in Q.
@@ -38,7 +41,10 @@
         return mgr;
     }
 
-    public void logAppLaunch(View v, Intent intent) { }
+    /**
+     * Logs app launches
+     */
+    public void logAppLaunch(View v, Intent intent, @Nullable UserHandle userHandle) { }
     public void logTaskLaunch(View v, ComponentKey key) { }
     public void logTaskDismiss(View v, ComponentKey key) { }
     public void logSwipeOnContainer(boolean isSwipingToLeft, int pageId) { }
diff --git a/src/com/android/launcher3/logging/UserEventDispatcher.java b/src/com/android/launcher3/logging/UserEventDispatcher.java
index 99906fe..8289da9 100644
--- a/src/com/android/launcher3/logging/UserEventDispatcher.java
+++ b/src/com/android/launcher3/logging/UserEventDispatcher.java
@@ -33,7 +33,9 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.os.Process;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.util.Log;
 import android.view.View;
 
@@ -135,7 +137,7 @@
     // --------------------------------------------------------------
 
     @Deprecated
-    public void logAppLaunch(View v, Intent intent) {
+    public void logAppLaunch(View v, Intent intent, @Nullable  UserHandle userHandle) {
         LauncherEvent event = newLauncherEvent(newTouchAction(Action.Touch.TAP),
                 newItemTarget(v, mInstantAppResolver), newTarget(Target.Type.CONTAINER));
 
@@ -143,7 +145,13 @@
             if (mDelegate != null) {
                 mDelegate.modifyUserEvent(event);
             }
-            fillIntentInfo(event.srcTarget[0], intent);
+            fillIntentInfo(event.srcTarget[0], intent, userHandle);
+        }
+        ItemInfo info = (ItemInfo) v.getTag();
+        if (Utilities.IS_DEBUG_DEVICE && FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
+            FileLog.d(TAG, "appLaunch: packageName:" + info.getTargetComponent().getPackageName()
+                    + ",isWorkApp:" + (info.user != null && !Process.myUserHandle().equals(
+                    userHandle)) + ",launchLocation:" + info.container);
         }
         dispatchUserEvent(event, intent);
         mAppOrTaskLaunch = true;
@@ -171,8 +179,9 @@
         mAppOrTaskLaunch = true;
     }
 
-    protected void fillIntentInfo(Target target, Intent intent) {
+    protected void fillIntentInfo(Target target, Intent intent, @Nullable UserHandle userHandle) {
         target.intentHash = intent.hashCode();
+        target.isWorkApp = userHandle != null && !userHandle.equals(Process.myUserHandle());
         fillComponentInfo(target, intent.getComponent());
     }
 
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 0e20270..c24b939 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -15,7 +15,11 @@
  */
 package com.android.launcher3.model;
 
+import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
+import static com.android.launcher3.shortcuts.ShortcutRequest.PINNED;
+
 import android.content.Context;
+import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
 import android.text.TextUtils;
@@ -29,15 +33,15 @@
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.PromiseAppInfo;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.DumpTargetWrapper;
 import com.android.launcher3.model.nano.LauncherDumpProto;
 import com.android.launcher3.model.nano.LauncherDumpProto.ContainerType;
 import com.android.launcher3.model.nano.LauncherDumpProto.DumpTarget;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
@@ -59,6 +63,8 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
 
 /**
  * All the data stored in-memory and managed by the LauncherModel
@@ -287,7 +293,7 @@
                     if ((count == null || --count.value == 0)
                             && !InstallShortcutReceiver.getPendingShortcuts(context)
                                 .contains(pinnedShortcut)) {
-                        DeepShortcutManager.getInstance(context).unpinShortcut(pinnedShortcut);
+                        unpinShortcut(context, pinnedShortcut);
                     }
                     // Fall through.
                 }
@@ -324,7 +330,7 @@
 
                 // Since this is a new item, pin the shortcut in the system server.
                 if (newItem && count.value == 1) {
-                    DeepShortcutManager.getInstance(context).pinShortcut(pinnedShortcut);
+                    updatePinnedShortcuts(context, pinnedShortcut, List::add);
                 }
                 // Fall through
             }
@@ -355,6 +361,36 @@
     }
 
     /**
+     * Removes the given shortcut from the current list of pinned shortcuts.
+     * (Runs on background thread)
+     */
+    public void unpinShortcut(Context context, ShortcutKey key) {
+        updatePinnedShortcuts(context, key, List::remove);
+    }
+
+    private void updatePinnedShortcuts(Context context, ShortcutKey key,
+            BiConsumer<List<String>, String> idOp) {
+        if (GO_DISABLE_WIDGETS) {
+            return;
+        }
+        String packageName = key.componentName.getPackageName();
+        String id = key.getId();
+        UserHandle user = key.user;
+        List<String> pinnedIds = new ShortcutRequest(context, user)
+                .forPackage(packageName)
+                .query(PINNED)
+                .stream()
+                .map(ShortcutInfo::getId)
+                .collect(Collectors.toCollection(ArrayList::new));
+        idOp.accept(pinnedIds, id);
+        try {
+            context.getSystemService(LauncherApps.class).pinShortcuts(packageName, pinnedIds, user);
+        } catch (SecurityException | IllegalStateException e) {
+            Log.w(TAG, "Failed to pin shortcut", e);
+        }
+    }
+
+    /**
      * Return an existing FolderInfo object if we have encountered this ID previously,
      * or make a new one.
      */
@@ -400,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/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 605bb75..571d41a 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
 import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
 import static com.android.launcher3.util.PackageManagerHelper.isSystemApp;
 
 import android.appwidget.AppWidgetProviderInfo;
@@ -72,8 +73,9 @@
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.provider.ImportDataTask;
 import com.android.launcher3.qsb.QsbContainerView;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IOUtils;
 import com.android.launcher3.util.LooperIdleLock;
@@ -114,7 +116,6 @@
     private final UserManager mUserManager;
     private final UserCache mUserCache;
 
-    private final DeepShortcutManager mShortcutManager;
     private final InstallSessionHelper mSessionHelper;
     private final IconCache mIconCache;
 
@@ -130,7 +131,6 @@
         mLauncherApps = mApp.getContext().getSystemService(LauncherApps.class);
         mUserManager = mApp.getContext().getSystemService(UserManager.class);
         mUserCache = UserCache.INSTANCE.get(mApp.getContext());
-        mShortcutManager = DeepShortcutManager.getInstance(mApp.getContext());
         mSessionHelper = InstallSessionHelper.INSTANCE.get(mApp.getContext());
         mIconCache = mApp.getIconCache();
     }
@@ -349,8 +349,8 @@
 
                     // We can only query for shortcuts when the user is unlocked.
                     if (userUnlocked) {
-                        DeepShortcutManager.QueryResult pinnedShortcuts =
-                                mShortcutManager.queryForPinnedShortcuts(null, user);
+                        QueryResult pinnedShortcuts = new ShortcutRequest(context, user)
+                                .query(ShortcutRequest.PINNED);
                         if (pinnedShortcuts.wasSuccess()) {
                             for (ShortcutInfo shortcut : pinnedShortcuts) {
                                 shortcutKeyToPinnedShortcuts.put(ShortcutKey.fromInfo(shortcut),
@@ -786,7 +786,7 @@
                 if ((numTimesPinned == null || numTimesPinned.value == 0)
                         && !pendingShortcuts.contains(key)) {
                     // Shortcut is pinned but doesn't exist on the workspace; unpin it.
-                    mShortcutManager.unpinShortcut(key);
+                    mBgDataModel.unpinShortcut(context, key);
                 }
             }
 
@@ -884,12 +884,12 @@
     private List<ShortcutInfo> loadDeepShortcuts() {
         List<ShortcutInfo> allShortcuts = new ArrayList<>();
         mBgDataModel.deepShortcutMap.clear();
-        mBgDataModel.hasShortcutHostPermission = mShortcutManager.hasHostPermission();
+        mBgDataModel.hasShortcutHostPermission = hasShortcutsPermission(mApp.getContext());
         if (mBgDataModel.hasShortcutHostPermission) {
             for (UserHandle user : mUserCache.getUserProfiles()) {
                 if (mUserManager.isUserUnlocked(user)) {
-                    List<ShortcutInfo> shortcuts =
-                            mShortcutManager.queryForAllShortcuts(user);
+                    List<ShortcutInfo> shortcuts = new ShortcutRequest(mApp.getContext(), user)
+                            .query(ShortcutRequest.ALL);
                     allShortcuts.addAll(shortcuts);
                     mBgDataModel.updateDeepShortcutCounts(null, user, shortcuts);
                 }
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/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 3361ff0..48c56e9 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -41,7 +41,7 @@
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.FlagOp;
 import com.android.launcher3.util.IntSparseArrayMap;
@@ -208,10 +208,11 @@
                             if (si.isPromise() && isNewApkAvailable) {
                                 boolean isTargetValid = true;
                                 if (si.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
-                                    List<ShortcutInfo> shortcut = DeepShortcutManager
-                                            .getInstance(context).queryForPinnedShortcuts(
-                                                    cn.getPackageName(),
-                                                    Arrays.asList(si.getDeepShortcutId()), mUser);
+                                    List<ShortcutInfo> shortcut =
+                                            new ShortcutRequest(context, mUser)
+                                                    .forPackage(cn.getPackageName(),
+                                                            si.getDeepShortcutId())
+                                                    .query(ShortcutRequest.PINNED);
                                     if (shortcut.isEmpty()) {
                                         isTargetValid = false;
                                     } else {
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java
index 05225d4..b0e7a69 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.java
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.java
@@ -24,8 +24,8 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.MultiHashMap;
 
@@ -54,8 +54,6 @@
     @Override
     public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
         final Context context = app.getContext();
-        DeepShortcutManager deepShortcutManager = DeepShortcutManager.getInstance(context);
-
         // Find WorkspaceItemInfo's that have changed on the workspace.
         HashSet<ShortcutKey> removedKeys = new HashSet<>();
         MultiHashMap<ShortcutKey, WorkspaceItemInfo> keyToShortcutInfo = new MultiHashMap<>();
@@ -74,8 +72,9 @@
         final ArrayList<WorkspaceItemInfo> updatedWorkspaceItemInfos = new ArrayList<>();
         if (!keyToShortcutInfo.isEmpty()) {
             // Update the workspace to reflect the changes to updated shortcuts residing on it.
-            List<ShortcutInfo> shortcuts = deepShortcutManager.queryForFullDetails(
-                    mPackageName, new ArrayList<>(allIds), mUser);
+            List<ShortcutInfo> shortcuts = new ShortcutRequest(context, mUser)
+                    .forPackage(mPackageName, new ArrayList<>(allIds))
+                    .query(ShortcutRequest.ALL);
             for (ShortcutInfo fullDetails : shortcuts) {
                 ShortcutKey key = ShortcutKey.fromInfo(fullDetails);
                 List<WorkspaceItemInfo> workspaceItemInfos = keyToShortcutInfo.remove(key);
diff --git a/src/com/android/launcher3/model/UserLockStateChangedTask.java b/src/com/android/launcher3/model/UserLockStateChangedTask.java
index 694ae1a..d527423 100644
--- a/src/com/android/launcher3/model/UserLockStateChangedTask.java
+++ b/src/com/android/launcher3/model/UserLockStateChangedTask.java
@@ -27,8 +27,9 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.ShortcutKey;
+import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ItemInfoMatcher;
 
@@ -52,12 +53,11 @@
     public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
         Context context = app.getContext();
         boolean isUserUnlocked = context.getSystemService(UserManager.class).isUserUnlocked(mUser);
-        DeepShortcutManager deepShortcutManager = DeepShortcutManager.getInstance(context);
 
         HashMap<ShortcutKey, ShortcutInfo> pinnedShortcuts = new HashMap<>();
         if (isUserUnlocked) {
-            DeepShortcutManager.QueryResult shortcuts =
-                    deepShortcutManager.queryForPinnedShortcuts(null, mUser);
+            QueryResult shortcuts = new ShortcutRequest(context, mUser)
+                    .query(ShortcutRequest.PINNED);
             if (shortcuts.wasSuccess()) {
                 for (ShortcutInfo shortcut : shortcuts) {
                     pinnedShortcuts.put(ShortcutKey.fromInfo(shortcut), shortcut);
@@ -115,7 +115,8 @@
 
         if (isUserUnlocked) {
             dataModel.updateDeepShortcutCounts(
-                    null, mUser, deepShortcutManager.queryForAllShortcuts(mUser));
+                    null, mUser,
+                    new ShortcutRequest(context, mUser).query(ShortcutRequest.ALL));
         }
         bindDeepShortcuts(dataModel);
     }
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..b764a07 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();
 
@@ -339,7 +351,9 @@
     }
 
     public void applyNotificationInfos(List<NotificationInfo> notificationInfos) {
-        mNotificationItemView.applyNotificationInfos(notificationInfos);
+        if (mNotificationItemView != null) {
+            mNotificationItemView.applyNotificationInfos(notificationInfos);
+        }
     }
 
     private void updateHiddenShortcuts() {
diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java
index 80c6683..947f49d 100644
--- a/src/com/android/launcher3/popup/PopupPopulator.java
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -31,8 +31,8 @@
 import com.android.launcher3.notification.NotificationInfo;
 import com.android.launcher3.notification.NotificationKeyData;
 import com.android.launcher3.notification.NotificationListener;
-import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.DeepShortcutView;
+import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.ArrayList;
@@ -144,8 +144,9 @@
                 uiHandler.post(() -> container.applyNotificationInfos(infos));
             }
 
-            List<ShortcutInfo> shortcuts = DeepShortcutManager.getInstance(launcher)
-                    .queryForShortcutsContainer(activity, user);
+            List<ShortcutInfo> shortcuts = new ShortcutRequest(launcher, user)
+                    .withContainer(activity)
+                    .query(ShortcutRequest.PUBLISHED);
             String shortcutIdToDeDupe = notificationKeys.isEmpty() ? null
                     : notificationKeys.get(0).shortcutId;
             shortcuts = PopupPopulator.sortAndFilterShortcuts(shortcuts, shortcutIdToDeDupe);
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 fb33551..9987994 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -34,8 +34,8 @@
 import com.android.launcher3.LauncherAppWidgetInfo;
 import com.android.launcher3.LauncherProvider.DatabaseHelper;
 import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.WorkspaceItemInfo;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.util.IntArray;
@@ -112,9 +112,16 @@
             whereClause.append(" AND profileId != ?");
             profileIds[i] = Long.toString(profileMapping.keyAt(i));
         }
-        int itemsDeleted = db.delete(Favorites.TABLE_NAME, whereClause.toString(), profileIds);
-        if (itemsDeleted > 0) {
+        try {
+            int itemsDeleted = db.delete(Favorites.TABLE_NAME, whereClause.toString(), profileIds);
             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: '")
+                    .append(whereClause).append("', profile Id size:").append(profileIds.length)
+                    .append("profileIds: ").append(String.join(", ", profileIds)).toString()
+            );
+            throw exception;
         }
 
         // Mark all items as restored.
diff --git a/src/com/android/launcher3/shortcuts/ShortcutKey.java b/src/com/android/launcher3/shortcuts/ShortcutKey.java
index 70665ca..fa1a85f 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutKey.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutKey.java
@@ -1,6 +1,7 @@
 package com.android.launcher3.shortcuts;
 
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
@@ -29,6 +30,14 @@
         return componentName.getClassName();
     }
 
+    /**
+     * Creates a {@link ShortcutRequest} for this key
+     */
+    public ShortcutRequest buildRequest(Context context) {
+        return new ShortcutRequest(context, user)
+                .forPackage(componentName.getPackageName(), getId());
+    }
+
     public static ShortcutKey fromInfo(ShortcutInfo shortcutInfo) {
         return new ShortcutKey(shortcutInfo.getPackage(), shortcutInfo.getUserHandle(),
                 shortcutInfo.getId());
diff --git a/src/com/android/launcher3/shortcuts/ShortcutRequest.java b/src/com/android/launcher3/shortcuts/ShortcutRequest.java
new file mode 100644
index 0000000..e6203b4
--- /dev/null
+++ b/src/com/android/launcher3/shortcuts/ShortcutRequest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.shortcuts;
+
+import static com.android.launcher3.model.WidgetsModel.GO_DISABLE_WIDGETS;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.LauncherApps;
+import android.content.pm.LauncherApps.ShortcutQuery;
+import android.content.pm.ShortcutInfo;
+import android.os.UserHandle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility class to streamline Shortcut query
+ */
+public class ShortcutRequest {
+
+    private static final String TAG = "ShortcutRequest";
+
+    public static final int ALL = ShortcutQuery.FLAG_MATCH_DYNAMIC
+            | ShortcutQuery.FLAG_MATCH_MANIFEST | ShortcutQuery.FLAG_MATCH_PINNED;
+    public static final int PUBLISHED = ShortcutQuery.FLAG_MATCH_DYNAMIC
+            | ShortcutQuery.FLAG_MATCH_MANIFEST;
+    public static final int PINNED = ShortcutQuery.FLAG_MATCH_PINNED;
+
+    private final ShortcutQuery mQuery = GO_DISABLE_WIDGETS ? null : new ShortcutQuery();
+
+    private final Context mContext;
+    private final UserHandle mUserHandle;
+
+    boolean mFailed = false;
+
+    public ShortcutRequest(Context context, UserHandle userHandle) {
+        mContext = context;
+        mUserHandle = userHandle;
+    }
+
+    public ShortcutRequest forPackage(String packageName, String... shortcutIds) {
+        return forPackage(packageName, Arrays.asList(shortcutIds));
+    }
+
+    public ShortcutRequest forPackage(String packageName, @Nullable List<String> shortcutIds) {
+        if (!GO_DISABLE_WIDGETS && packageName != null) {
+            mQuery.setPackage(packageName);
+            mQuery.setShortcutIds(shortcutIds);
+        }
+        return this;
+    }
+
+    public ShortcutRequest withContainer(@Nullable ComponentName activity) {
+        if (!GO_DISABLE_WIDGETS) {
+            if (activity == null) {
+                mFailed = true;
+            } else {
+                mQuery.setActivity(activity);
+            }
+        }
+        return this;
+    }
+
+    public QueryResult query(int flags) {
+        if (GO_DISABLE_WIDGETS || mFailed) {
+            return QueryResult.DEFAULT;
+        }
+        mQuery.setQueryFlags(flags);
+
+        try {
+            return new QueryResult(mContext.getSystemService(LauncherApps.class)
+                    .getShortcuts(mQuery, mUserHandle));
+        } catch (SecurityException | IllegalStateException e) {
+            Log.e(TAG, "Failed to query for shortcuts", e);
+            return QueryResult.DEFAULT;
+        }
+    }
+
+    public static class QueryResult extends ArrayList<ShortcutInfo> {
+
+        static final QueryResult DEFAULT = new QueryResult(GO_DISABLE_WIDGETS);
+
+        private final boolean mWasSuccess;
+
+        QueryResult(List<ShortcutInfo> result) {
+            super(result == null ? Collections.emptyList() : result);
+            mWasSuccess = true;
+        }
+
+        QueryResult(boolean wasSuccess) {
+            mWasSuccess = wasSuccess;
+        }
+
+
+        public boolean wasSuccess() {
+            return mWasSuccess;
+        }
+    }
+}
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/TestLogging.java b/src/com/android/launcher3/testing/TestLogging.java
new file mode 100644
index 0000000..024110d
--- /dev/null
+++ b/src/com/android/launcher3/testing/TestLogging.java
@@ -0,0 +1,29 @@
+/*
+ * 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.testing;
+
+import android.util.Log;
+
+import com.android.launcher3.Utilities;
+
+public final class TestLogging {
+    public static synchronized void recordEvent(String event) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            Log.d(TestProtocol.TAPL_EVENTS_TAG, event);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 5aae841..01c207f 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -31,6 +31,7 @@
     public static final int QUICK_SWITCH_STATE_ORDINAL = 4;
     public static final int ALL_APPS_STATE_ORDINAL = 5;
     public static final int BACKGROUND_APP_STATE_ORDINAL = 6;
+    public static final String TAPL_EVENTS_TAG = "TaplEvents";
 
     public static String stateOrdinalToString(int ordinal) {
         switch (ordinal) {
@@ -73,6 +74,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 +88,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..d193bef 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();
@@ -508,7 +508,7 @@
             mAtomicComponentsController.getAnimationPlayer().end();
             mAtomicComponentsController = null;
         }
-        cancelAnimationControllers();
+        clearState();
         boolean shouldGoToTargetState = true;
         if (mPendingAnimation != null) {
             boolean reachedTarget = mToState == targetState;
@@ -546,13 +546,13 @@
             mAtomicAnim = null;
         }
         mScheduleResumeAtomicComponent = false;
+        mDetector.finishedScrolling();
+        mDetector.setDetectableScrollConditions(0, false);
     }
 
     private void cancelAnimationControllers() {
         mCurrentAnimation = null;
         cancelAtomicComponentsController();
-        mDetector.finishedScrolling();
-        mDetector.setDetectableScrollConditions(0, false);
     }
 
     private void cancelAtomicComponentsController() {
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
index 12ca5ee..30283da 100644
--- a/src/com/android/launcher3/touch/BaseSwipeDetector.java
+++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java
@@ -24,6 +24,10 @@
 import android.view.ViewConfiguration;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.LinkedList;
+import java.util.Queue;
 
 /**
  * Scroll/drag/swipe gesture detector.
@@ -49,13 +53,15 @@
     protected final boolean mIsRtl;
     protected final float mTouchSlop;
     protected final float mMaxVelocity;
+    private final Queue<Runnable> mSetStateQueue = new LinkedList<>();
 
     private int mActivePointerId = INVALID_POINTER_ID;
     private VelocityTracker mVelocityTracker;
     private PointF mLastDisplacement = new PointF();
     private PointF mDisplacement = new PointF();
     protected PointF mSubtractDisplacement = new PointF();
-    private ScrollState mState = ScrollState.IDLE;
+    @VisibleForTesting ScrollState mState = ScrollState.IDLE;
+    private boolean mIsSettingState;
 
     protected boolean mIgnoreSlopWhenSettling;
 
@@ -195,6 +201,12 @@
     // SETTLING -> (View settled) -> IDLE
 
     private void setState(ScrollState newState) {
+        if (mIsSettingState) {
+            mSetStateQueue.add(() -> setState(newState));
+            return;
+        }
+        mIsSettingState = true;
+
         if (DBG) {
             Log.d(TAG, "setState:" + mState + "->" + newState);
         }
@@ -212,6 +224,10 @@
         }
 
         mState = newState;
+        mIsSettingState = false;
+        if (!mSetStateQueue.isEmpty()) {
+            mSetStateQueue.remove().run();
+        }
     }
 
     private void initializeDragging() {
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index ba1bfa5..bee7853 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -33,6 +33,7 @@
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.folder.Folder;
+import com.android.launcher3.testing.TestLogging;
 
 /**
  * Class to handle long-clicks on workspace items and start drag as a result.
@@ -46,6 +47,7 @@
             ItemLongClickListener::onAllAppsItemLongClick;
 
     private static boolean onWorkspaceItemLongClick(View v) {
+        TestLogging.recordEvent("onWorkspaceItemLongClick");
         Launcher launcher = Launcher.getLauncher(v.getContext());
         if (!canStartDrag(launcher)) return false;
         if (!launcher.isInState(NORMAL) && !launcher.isInState(OVERVIEW)) return false;
@@ -75,6 +77,7 @@
     }
 
     private static boolean onAllAppsItemLongClick(View v) {
+        TestLogging.recordEvent("onAllAppsItemLongClick");
         Launcher launcher = Launcher.getLauncher(v.getContext());
         if (!canStartDrag(launcher)) return false;
         // When we have exited all apps or are in transition, disregard long clicks
diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
index 66fdc94..61ce046 100644
--- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java
+++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
@@ -25,7 +25,6 @@
 
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.util.Log;
 import android.view.GestureDetector;
 import android.view.HapticFeedbackConstants;
 import android.view.MotionEvent;
@@ -37,10 +36,9 @@
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.testing.TestProtocol;
+import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.views.OptionsPopupView;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -168,6 +166,7 @@
 
     @Override
     public void onLongPress(MotionEvent event) {
+        TestLogging.recordEvent("Workspace.longPress");
         if (mLongPressState == STATE_REQUESTED) {
             if (canHandleLongPress()) {
                 mLongPressState = STATE_PENDING_PARENT_INFORM;
@@ -178,9 +177,6 @@
                 mLauncher.getUserEventDispatcher().logActionOnContainer(Action.Touch.LONGPRESS,
                         Action.Direction.NONE, ContainerType.WORKSPACE,
                         mWorkspace.getCurrentPage());
-                if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
-                    Log.d(TestProtocol.PERMANENT_DIAG_TAG, "Opening options popup on long press");
-                }
                 OptionsPopupView.showDefaultOptions(mLauncher, mTouchDownPoint.x, mTouchDownPoint.y);
             } else {
                 cancelLongPress();
diff --git a/src/com/android/launcher3/util/Executors.java b/src/com/android/launcher3/util/Executors.java
index 4d5ee49..0a32734 100644
--- a/src/com/android/launcher3/util/Executors.java
+++ b/src/com/android/launcher3/util/Executors.java
@@ -19,7 +19,6 @@
 import android.os.Looper;
 import android.os.Process;
 
-import java.util.concurrent.Executor;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -36,9 +35,9 @@
     private static final int KEEP_ALIVE = 1;
 
     /**
-     * An {@link Executor} to be used with async task with no limit on the queue size.
+     * An {@link ThreadPoolExecutor} to be used with async task with no limit on the queue size.
      */
-    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
+    public static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
             CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
             TimeUnit.SECONDS, new LinkedBlockingQueue<>());
 
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/LooperExecutor.java b/src/com/android/launcher3/util/LooperExecutor.java
index 8ac600f..3a8a13c 100644
--- a/src/com/android/launcher3/util/LooperExecutor.java
+++ b/src/com/android/launcher3/util/LooperExecutor.java
@@ -41,10 +41,10 @@
 
     @Override
     public void execute(Runnable runnable) {
-        if (mHandler.getLooper() == Looper.myLooper()) {
+        if (getHandler().getLooper() == Looper.myLooper()) {
             runnable.run();
         } else {
-            mHandler.post(runnable);
+            getHandler().post(runnable);
         }
     }
 
@@ -52,7 +52,7 @@
      * Same as execute, but never runs the action inline.
      */
     public void post(Runnable runnable) {
-        mHandler.post(runnable);
+        getHandler().post(runnable);
     }
 
     /**
@@ -96,14 +96,14 @@
      * Returns the thread for this executor
      */
     public Thread getThread() {
-        return mHandler.getLooper().getThread();
+        return getHandler().getLooper().getThread();
     }
 
     /**
      * Returns the looper for this executor
      */
     public Looper getLooper() {
-        return mHandler.getLooper();
+        return getHandler().getLooper();
     }
 
     /**
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 2d56ce7..8b2ee36 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -332,4 +332,17 @@
         }
         return false;
     }
+
+    /**
+     * Returns true if Launcher has the permission to access shortcuts.
+     * @see LauncherApps#hasShortcutHostPermission()
+     */
+    public static boolean hasShortcutsPermission(Context context) {
+        try {
+            return context.getSystemService(LauncherApps.class).hasShortcutHostPermission();
+        } catch (SecurityException | IllegalStateException e) {
+            Log.e(TAG, "Failed to make shortcut manager call", e);
+        }
+        return false;
+    }
 }
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/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 6cae43d..3758cb8 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -35,6 +35,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.uioverrides.WallpaperColorInfo;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -90,6 +91,7 @@
 
     @Override
     public boolean onLongClick(View v) {
+        TestLogging.recordEvent("Widgets.onLongClick");
         if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
 
         if (v instanceof WidgetCell) {
diff --git a/src/com/android/launcher3/widget/WidgetsFullSheet.java b/src/com/android/launcher3/widget/WidgetsFullSheet.java
index 521f511..2a102d2 100644
--- a/src/com/android/launcher3/widget/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsFullSheet.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.widget;
 
+import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
@@ -37,6 +39,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetHost.ProviderChangedListener;
 import com.android.launcher3.R;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 import com.android.launcher3.views.TopRoundedCornerView;
 
@@ -247,4 +250,10 @@
         anim.play(ObjectAnimator.ofFloat(mRecyclerView, ALPHA, 0.5f));
         return anim;
     }
+
+    @Override
+    protected void onCloseComplete() {
+        super.onCloseComplete();
+        AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL);
+    }
 }
diff --git a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
index 60eb304..28a9193 100644
--- a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
+++ b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
@@ -24,11 +24,11 @@
  * the user to a more recent app).
  */
 @ProvidesInterface(action = com.android.systemui.plugins.OverscrollPlugin.ACTION,
-        version = com.android.systemui.plugins.OverlayPlugin.VERSION)
+        version = com.android.systemui.plugins.OverscrollPlugin.VERSION)
 public interface OverscrollPlugin extends Plugin {
 
     String ACTION = "com.android.systemui.action.PLUGIN_LAUNCHER_OVERSCROLL";
-    int VERSION = 1;
+    int VERSION = 3;
 
     String DEVICE_STATE_LOCKED = "Locked";
     String DEVICE_STATE_LAUNCHER = "Launcher";
@@ -36,9 +36,38 @@
     String DEVICE_STATE_UNKNOWN = "Unknown";
 
     /**
-     * Called when the user completed a right to left swipe in the gesture area.
-     *
-     * @param deviceState One of the DEVICE_STATE_* constants.
+     * @return true if the plugin is active and will accept overscroll gestures
      */
-    void onOverscroll(String deviceState);
+    boolean isActive();
+
+    /**
+     * Called when a touch is down and has been recognized as an overscroll gesture.
+     * A call of this method will always result in `onTouchUp` being called, and possibly
+     * `onFling` as well.
+     *
+     * @param deviceState String representing the current device state
+     * @param underlyingActivity String representing the currently active Activity
+     */
+    void onTouchStart(String deviceState, String underlyingActivity);
+
+    /**
+     * Called when a touch that was previously recognized has moved.
+     *
+     * @param px distance between the position of touch on this update and the position of the
+     * touch when it was initially recognized.
+     */
+    void onTouchTraveled(int px);
+
+    /**
+     * Called when a touch that was previously recognized has ended.
+     *
+     * @param px distance between the position of touch on this update and the position of the
+     * touch when it was initially recognized.
+     */
+    void onTouchEnd(int px);
+
+    /**
+     * Called when the user starts Compose with a fling. `onTouchUp` will also be called.
+     */
+    void onFling(float velocity);
 }
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/src_shortcuts_overrides/com/android/launcher3/shortcuts/DeepShortcutManager.java b/src_shortcuts_overrides/com/android/launcher3/shortcuts/DeepShortcutManager.java
deleted file mode 100644
index 57f4164..0000000
--- a/src_shortcuts_overrides/com/android/launcher3/shortcuts/DeepShortcutManager.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright (C) 2016 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.shortcuts;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.LauncherApps;
-import android.content.pm.LauncherApps.ShortcutQuery;
-import android.content.pm.ShortcutInfo;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Performs operations related to deep shortcuts, such as querying for them, pinning them, etc.
- */
-public class DeepShortcutManager {
-    private static final String TAG = "DeepShortcutManager";
-
-    private static final int FLAG_GET_ALL = ShortcutQuery.FLAG_MATCH_DYNAMIC
-            | ShortcutQuery.FLAG_MATCH_MANIFEST | ShortcutQuery.FLAG_MATCH_PINNED;
-
-    private static DeepShortcutManager sInstance;
-
-    public static DeepShortcutManager getInstance(Context context) {
-        if (sInstance == null) {
-            sInstance = new DeepShortcutManager(context.getApplicationContext());
-        }
-        return sInstance;
-    }
-
-    private final LauncherApps mLauncherApps;
-
-    private DeepShortcutManager(Context context) {
-        mLauncherApps = (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
-    }
-
-    /**
-     * Queries for the shortcuts with the package name and provided ids.
-     *
-     * This method is intended to get the full details for shortcuts when they are added or updated,
-     * because we only get "key" fields in onShortcutsChanged().
-     */
-    public QueryResult queryForFullDetails(String packageName,
-            List<String> shortcutIds, UserHandle user) {
-        return query(FLAG_GET_ALL, packageName, null, shortcutIds, user);
-    }
-
-    /**
-     * Gets all the manifest and dynamic shortcuts associated with the given package and user,
-     * to be displayed in the shortcuts container on long press.
-     */
-    public QueryResult queryForShortcutsContainer(@Nullable ComponentName activity,
-            UserHandle user) {
-        if (activity == null) return QueryResult.FAILURE;
-        return query(ShortcutQuery.FLAG_MATCH_MANIFEST | ShortcutQuery.FLAG_MATCH_DYNAMIC,
-                activity.getPackageName(), activity, null, user);
-    }
-
-    /**
-     * Removes the given shortcut from the current list of pinned shortcuts.
-     * (Runs on background thread)
-     */
-    public void unpinShortcut(final ShortcutKey key) {
-        String packageName = key.componentName.getPackageName();
-        String id = key.getId();
-        UserHandle user = key.user;
-        List<String> pinnedIds = extractIds(queryForPinnedShortcuts(packageName, user));
-        pinnedIds.remove(id);
-        try {
-            mLauncherApps.pinShortcuts(packageName, pinnedIds, user);
-        } catch (SecurityException|IllegalStateException e) {
-            Log.w(TAG, "Failed to unpin shortcut", e);
-        }
-    }
-
-    /**
-     * Adds the given shortcut to the current list of pinned shortcuts.
-     * (Runs on background thread)
-     */
-    public void pinShortcut(final ShortcutKey key) {
-        String packageName = key.componentName.getPackageName();
-        String id = key.getId();
-        UserHandle user = key.user;
-        List<String> pinnedIds = extractIds(queryForPinnedShortcuts(packageName, user));
-        pinnedIds.add(id);
-        try {
-            mLauncherApps.pinShortcuts(packageName, pinnedIds, user);
-        } catch (SecurityException|IllegalStateException e) {
-            Log.w(TAG, "Failed to pin shortcut", e);
-        }
-    }
-
-    public void startShortcut(String packageName, String id, Rect sourceBounds,
-          Bundle startActivityOptions, UserHandle user) {
-        try {
-            mLauncherApps.startShortcut(packageName, id, sourceBounds,
-                    startActivityOptions, user);
-        } catch (SecurityException|IllegalStateException e) {
-            Log.e(TAG, "Failed to start shortcut", e);
-        }
-    }
-
-    public Drawable getShortcutIconDrawable(ShortcutInfo shortcutInfo, int density) {
-        try {
-            return mLauncherApps.getShortcutIconDrawable(shortcutInfo, density);
-        } catch (SecurityException|IllegalStateException e) {
-            Log.e(TAG, "Failed to get shortcut icon", e);
-            return null;
-        }
-    }
-
-    /**
-     * Returns the id's of pinned shortcuts associated with the given package and user.
-     *
-     * If packageName is null, returns all pinned shortcuts regardless of package.
-     */
-    public QueryResult queryForPinnedShortcuts(String packageName, UserHandle user) {
-        return queryForPinnedShortcuts(packageName, null, user);
-    }
-
-    public QueryResult queryForPinnedShortcuts(String packageName, List<String> shortcutIds,
-            UserHandle user) {
-        return query(ShortcutQuery.FLAG_MATCH_PINNED, packageName, null, shortcutIds, user);
-    }
-
-    public QueryResult queryForAllShortcuts(UserHandle user) {
-        return query(FLAG_GET_ALL, null, null, null, user);
-    }
-
-    private static List<String> extractIds(List<ShortcutInfo> shortcuts) {
-        List<String> shortcutIds = new ArrayList<>(shortcuts.size());
-        for (ShortcutInfo shortcut : shortcuts) {
-            shortcutIds.add(shortcut.getId());
-        }
-        return shortcutIds;
-    }
-
-    /**
-     * Query the system server for all the shortcuts matching the given parameters.
-     * If packageName == null, we query for all shortcuts with the passed flags, regardless of app.
-     *
-     * TODO: Use the cache to optimize this so we don't make an RPC every time.
-     */
-    private QueryResult query(int flags, String packageName, ComponentName activity,
-            List<String> shortcutIds, UserHandle user) {
-        ShortcutQuery q = new ShortcutQuery();
-        q.setQueryFlags(flags);
-        if (packageName != null) {
-            q.setPackage(packageName);
-            q.setActivity(activity);
-            q.setShortcutIds(shortcutIds);
-        }
-        try {
-            return new QueryResult(mLauncherApps.getShortcuts(q, user));
-        } catch (SecurityException|IllegalStateException e) {
-            Log.e(TAG, "Failed to query for shortcuts", e);
-            return QueryResult.FAILURE;
-        }
-    }
-
-    public boolean hasHostPermission() {
-        try {
-            return mLauncherApps.hasShortcutHostPermission();
-        } catch (SecurityException|IllegalStateException e) {
-            Log.e(TAG, "Failed to make shortcut manager call", e);
-        }
-        return false;
-    }
-
-    public static class QueryResult extends ArrayList<ShortcutInfo> {
-
-        static QueryResult FAILURE = new QueryResult();
-
-        private final boolean mWasSuccess;
-
-        QueryResult(List<ShortcutInfo> result) {
-            super(result == null ? Collections.emptyList() : result);
-            mWasSuccess = true;
-        }
-
-        QueryResult() {
-            mWasSuccess = false;
-        }
-
-
-        public boolean wasSuccess() {
-            return mWasSuccess;
-        }
-    }
-}
diff --git a/tests/Android.mk b/tests/Android.mk
index 83fdddc..d1a6c06 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -57,7 +57,7 @@
     LOCAL_PRIVATE_PLATFORM_APIS := true
     LOCAL_STATIC_JAVA_LIBRARIES += launcher-aosp-tapl
 else
-    LOCAL_SDK_VERSION := 28
+    LOCAL_SDK_VERSION := system_28
     LOCAL_MIN_SDK_VERSION := 21
     LOCAL_STATIC_JAVA_LIBRARIES += ub-launcher-aosp-tapl
 endif
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/touch/SingleAxisSwipeDetectorTest.java b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
index 5174e4d..6d463b5 100644
--- a/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
+++ b/tests/src/com/android/launcher3/touch/SingleAxisSwipeDetectorTest.java
@@ -21,9 +21,11 @@
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL;
 
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.anyBoolean;
 import static org.mockito.Matchers.anyFloat;
 import static org.mockito.Matchers.anyObject;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -168,4 +170,21 @@
         // TODO: actually calculate the following parameters and do exact value checks.
         verify(mMockListener).onDragEnd(anyFloat());
     }
+
+    @Test
+    public void testInterleavedSetState() {
+        doAnswer(invocationOnMock -> {
+            // Sets state to IDLE. (Normally onDragEnd() will have state SETTLING.)
+            mDetector.finishedScrolling();
+            return null;
+        }).when(mMockListener).onDragEnd(anyFloat());
+
+        mGenerator.put(0, 100, 100);
+        mGenerator.move(0, 100, 100 + mTouchSlop);
+        mGenerator.move(0, 100, 100 + mTouchSlop * 2);
+        mGenerator.lift(0);
+        verify(mMockListener).onDragEnd(anyFloat());
+        assertTrue("SwipeDetector should be IDLE but was " + mDetector.mState,
+                mDetector.isIdleState());
+    }
 }
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 4243ed0..60dad12 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -92,7 +92,7 @@
     public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
     public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5;
 
-    public static final long DEFAULT_UI_TIMEOUT = 60000; // b/136278866
+    public static final long DEFAULT_UI_TIMEOUT = 10000;
     private static final String TAG = "AbstractLauncherUiTest";
 
     protected LooperExecutor mMainThreadExecutor = MAIN_EXECUTOR;
@@ -259,7 +259,7 @@
     protected <T> T getOnUiThread(final Callable<T> callback) {
         try {
             return mMainThreadExecutor.submit(callback).get();
-        } catch (Exception e) {
+        } catch (Throwable e) {
             throw new RuntimeException(e);
         }
     }
diff --git a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
index 80bb3ed..1a68122 100644
--- a/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
+++ b/tests/src/com/android/launcher3/ui/PortraitLandscapeRunner.java
@@ -38,8 +38,8 @@
 
                     evaluateInPortrait();
                     evaluateInLandscape();
-                } catch (Exception e) {
-                    Log.e(TAG, "Exception", e);
+                } catch (Throwable e) {
+                    Log.e(TAG, "Error", e);
                     throw e;
                 } finally {
                     mTest.mDevice.setOrientationNatural();
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/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index 0472ce1..62e2a53 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -103,11 +103,11 @@
 
         setResult(acceptConfig);
         if (acceptConfig) {
-            Wait.atMost(null, new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
+            Wait.atMost("", new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
             assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
         } else {
             // Verify that the widget id is deleted.
-            Wait.atMost(null, () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
+            Wait.atMost("", () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
                     DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
         }
     }
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index d909ad7..59b861c 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -170,7 +170,7 @@
 
         // Go back to home
         mLauncher.pressHome();
-        Wait.atMost(null, new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT,
+        Wait.atMost("", new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT,
                 mLauncher);
     }
 
diff --git a/tests/src/com/android/launcher3/util/Wait.java b/tests/src/com/android/launcher3/util/Wait.java
index 2663d02..2ab1e00 100644
--- a/tests/src/com/android/launcher3/util/Wait.java
+++ b/tests/src/com/android/launcher3/util/Wait.java
@@ -7,6 +7,8 @@
 
 import org.junit.Assert;
 
+import java.util.function.Supplier;
+
 /**
  * A utility class for waiting for a condition to be true.
  */
@@ -16,10 +18,16 @@
 
     public static void atMost(String message, Condition condition, long timeout,
             LauncherInstrumentation launcher) {
+        atMost(() -> message, condition, timeout, DEFAULT_SLEEP_MS, launcher);
+    }
+
+    public static void atMost(Supplier<String> message, Condition condition, long timeout,
+            LauncherInstrumentation launcher) {
         atMost(message, condition, timeout, DEFAULT_SLEEP_MS, launcher);
     }
 
-    public static void atMost(String message, Condition condition, long timeout, long sleepMillis,
+    public static void atMost(Supplier<String> message, Condition condition, long timeout,
+            long sleepMillis,
             LauncherInstrumentation launcher) {
         final long startTime = SystemClock.uptimeMillis();
         long endTime = startTime + timeout;
@@ -45,6 +53,6 @@
         }
         Log.d("Wait", "atMost: timed out: " + SystemClock.uptimeMillis());
         launcher.checkForAnomaly();
-        Assert.fail(message);
+        Assert.fail(message.get());
     }
 }
diff --git a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
index f98957e..b394bcb 100644
--- a/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/TestStabilityRule.java
@@ -102,14 +102,15 @@
 
         final String launcherVersion;
         try {
+            final String launcherPackageName = UiDevice.getInstance(getInstrumentation())
+                    .getLauncherPackageName();
+            Log.d(TAG, "Launcher package: " + launcherPackageName);
+
             launcherVersion = getInstrumentation().
                     getContext().
                     getPackageManager().
-                    getPackageInfo(
-                            UiDevice.getInstance(getInstrumentation()).
-                                    getLauncherPackageName(),
-                            0).
-                    versionName;
+                    getPackageInfo(launcherPackageName, 0)
+                    .versionName;
         } catch (PackageManager.NameNotFoundException e) {
             throw new RuntimeException(e);
         }
@@ -148,7 +149,8 @@
             Log.d(TAG, "PLATFORM PRESUBMIT");
             runFlavor = PLATFORM_PRESUBMIT;
         } else if (launcherBuildMatcher.group("platform") != null
-                && platformBuildMatcher.group("postsubmit") != null) {
+                && (platformBuildMatcher.group("postsubmit") != null
+                || platformBuildMatcher.group("commandLine") != null)) {
             Log.d(TAG, "PLATFORM POSTSUBMIT");
             runFlavor = PLATFORM_POSTSUBMIT;
         } else {
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 44fc3f7..bf68f71 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -16,19 +16,21 @@
 
 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;
 import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.UiObject2;
 
+import java.util.regex.Pattern;
+
 /**
  * App icon, whether in all apps or in workspace/
  */
 public final class AppIcon extends Launchable {
+
+    private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onAllAppsItemLongClick");
+
     AppIcon(LauncherInstrumentation launcher, UiObject2 icon) {
         super(launcher, icon);
     }
@@ -41,14 +43,13 @@
      * 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
+    protected void addExpectedEventsForLongClick() {
+        mLauncher.expectEvent(LONG_CLICK_EVENT);
     }
 
     @Override
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
index ba9c10e..597be90 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIconMenuItem.java
@@ -34,6 +34,10 @@
     }
 
     @Override
+    protected void addExpectedEventsForLongClick() {
+    }
+
+    @Override
     protected String getLongPressIndicator() {
         return "drop_target_bar";
     }
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/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 6881197..9327cb3 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -69,18 +69,24 @@
      * Drags an object to the center of homescreen.
      */
     public void dragToWorkspace() {
-        final Point launchableCenter = getObject().getVisibleCenter();
-        final Point displaySize = mLauncher.getRealDisplaySize();
-        final int width = displaySize.x / 2;
-        Workspace.dragIconToWorkspace(
-                mLauncher,
-                this,
-                new Point(
-                        launchableCenter.x >= width ?
-                                launchableCenter.x - width / 2 : launchableCenter.x + width / 2,
-                        displaySize.y / 2),
-                getLongPressIndicator());
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            final Point launchableCenter = getObject().getVisibleCenter();
+            final Point displaySize = mLauncher.getRealDisplaySize();
+            final int width = displaySize.x / 2;
+            addExpectedEventsForLongClick();
+            Workspace.dragIconToWorkspace(
+                    mLauncher,
+                    this,
+                    new Point(
+                            launchableCenter.x >= width
+                                    ? launchableCenter.x - width / 2
+                                    : launchableCenter.x + width / 2,
+                            displaySize.y / 2),
+                    getLongPressIndicator());
+        }
     }
 
+    protected abstract void addExpectedEventsForLongClick();
+
     protected abstract String getLongPressIndicator();
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 2fea1b7..196c6b7 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;
@@ -78,6 +79,8 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 /**
@@ -91,6 +94,13 @@
     private static final int GESTURE_STEP_MS = 16;
     private static long START_TIME = System.currentTimeMillis();
 
+    static final Pattern LOG_TIME = Pattern.compile(
+            "[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9][0-9][0-9]");
+
+    static final Pattern EVENT_LOG_ENTRY = Pattern.compile(
+            "(?<time>[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]\\.[0-9][0-9][0-9])"
+                    + ".*" + TestProtocol.TAPL_EVENTS_TAG + ": (?<event>.*)");
+
     // Types for launcher containers that the user is interacting with. "Background" is a
     // pseudo-container corresponding to inactive launcher covered by another app.
     public enum ContainerType {
@@ -130,6 +140,7 @@
     private static final String APPS_RES_ID = "apps_view";
     private static final String OVERVIEW_RES_ID = "overview_panel";
     private static final String WIDGETS_RES_ID = "widgets_list_view";
+    private static final String CONTEXT_MENU_RES_ID = "deep_shortcuts_container";
     public static final int WAIT_TIME_MS = 10000;
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
 
@@ -144,6 +155,11 @@
 
     private Consumer<ContainerType> mOnSettledStateAction;
 
+    // Not null when we are collecting expected events to compare with actual ones.
+    private List<Pattern> mExpectedEvents;
+
+    private String mTimeBeforeFirstLogEvent;
+
     /**
      * Constructs the root of TAPL hierarchy. You get all other objects from it.
      */
@@ -182,13 +198,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(
@@ -259,9 +270,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();
         };
     }
@@ -302,17 +313,39 @@
     public void checkForAnomaly() {
         final String anomalyMessage = getAnomalyMessage();
         if (anomalyMessage != null) {
-            failWithSystemHealth(
-                    "Tests are broken by a non-Launcher system error: " + anomalyMessage);
+            String message = "Tests are broken by a non-Launcher system error: " + anomalyMessage;
+            log("Hierarchy dump for: " + message);
+            dumpViewHierarchy();
+
+            Assert.fail(formatSystemHealthMessage(message));
+        }
+    }
+
+    private String getVisiblePackages() {
+        return mDevice.findObjects(By.textStartsWith(""))
+                .stream()
+                .map(LauncherInstrumentation::getApplicationPackageSafe)
+                .distinct()
+                .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";
         if (hasLauncherObject(OVERVIEW_RES_ID)) return "Overview";
         if (hasLauncherObject(WORKSPACE_RES_ID)) return "Workspace";
         if (hasLauncherObject(APPS_RES_ID)) return "AllApps";
-        return "Background";
+        return "Background (" + getVisiblePackages() + ")";
     }
 
     public void setSystemHealthSupplier(Function<Long, String> supplier) {
@@ -323,41 +356,42 @@
         mOnSettledStateAction = onSettledStateAction;
     }
 
-    private String getSystemHealthMessage() {
+    private String formatSystemHealthMessage(String message) {
         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();
-        }
 
-        return mSystemHealthSupplier != null
+        mInstrumentation.getUiAutomation().grantRuntimePermission(
+                testPackage, "android.permission.READ_LOGS");
+        mInstrumentation.getUiAutomation().grantRuntimePermission(
+                testPackage, "android.permission.PACKAGE_USAGE_STATS");
+
+        final String systemHealth = mSystemHealthSupplier != null
                 ? mSystemHealthSupplier.apply(START_TIME)
                 : TestHelpers.getSystemHealthMessage(getContext(), START_TIME);
+
+        if (systemHealth != null) {
+            return message
+                    + ",\nperhaps linked to system health problems:\n<<<<<<<<<<<<<<<<<<\n"
+                    + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
+        }
+
+        return message;
     }
 
     private void fail(String message) {
         checkForAnomaly();
 
-        failWithSystemHealth("http://go/tapl : " + getContextDescription() + message +
-                " (visible state: " + getVisibleStateMessage() + ")");
-    }
-
-    private void failWithSystemHealth(String message) {
-        final String systemHealth = getSystemHealthMessage();
-        if (systemHealth != null) {
-            message = message
-                    + ", perhaps because of system health problems:\n<<<<<<<<<<<<<<<<<<\n"
-                    + systemHealth + "\n>>>>>>>>>>>>>>>>>>";
-        }
-
+        message = "http://go/tapl : " + getContextDescription() + message
+                + " (visible state: " + getVisibleStateMessage() + ")";
         log("Hierarchy dump for: " + message);
         dumpViewHierarchy();
 
-        Assert.fail(message);
+        final String eventMismatch = getEventMismatchMessage();
+
+        if (eventMismatch != null) {
+            message = message + ",\nhaving produced wrong events:\n    " + eventMismatch;
+        }
+
+        Assert.fail(formatSystemHealthMessage(message));
     }
 
     private String getContextDescription() {
@@ -427,12 +461,6 @@
         assertEquals("Unexpected display rotation",
                 mExpectedRotation, mDevice.getDisplayRotation());
 
-        // b/136278866
-        for (int i = 0; i != 100; ++i) {
-            if (getNavigationModeMismatchError() == null) break;
-            sleep(100);
-        }
-
         final String error = getNavigationModeMismatchError();
         assertTrue(error, error == null);
         log("verifyContainerType: " + containerType);
@@ -517,7 +545,9 @@
                     mInstrumentation.getUiAutomation().executeAndWaitForEvent(
                             command, eventFilter, WAIT_TIME_MS);
             assertNotNull("executeAndWaitForEvent returned null (this can't happen)", event);
-            return event.getParcelableData();
+            final Parcelable parcelableData = event.getParcelableData();
+            event.recycle();
+            return parcelableData;
         } catch (TimeoutException e) {
             fail(message.get());
             return null;
@@ -541,7 +571,7 @@
 
             final Point displaySize = getRealDisplaySize();
 
-            if (hasLauncherObject("deep_shortcuts_container")) {
+            if (hasLauncherObject(CONTEXT_MENU_RES_ID)) {
                 linearGesture(
                         displaySize.x / 2, displaySize.y - 1,
                         displaySize.x / 2, 0,
@@ -549,13 +579,13 @@
                         false);
                 try (LauncherInstrumentation.Closable c = addContextLayer(
                         "Swiped up from context menu to home")) {
-                    waitUntilGone("deep_shortcuts_container");
+                    waitUntilGone(CONTEXT_MENU_RES_ID);
                 }
             }
             if (hasLauncherObject(WORKSPACE_RES_ID)) {
                 log(action = "already at home");
             } else {
-                log("Hierarchy before swiping up to home");
+                log("Hierarchy before swiping up to home:");
                 dumpViewHierarchy();
                 log(action = "swiping up to home from " + getVisibleStateMessage());
 
@@ -567,15 +597,19 @@
                 }
             }
         } else {
-            log(action = "clicking home button");
-            executeAndWaitForEvent(
-                    () -> {
-                        log("LauncherInstrumentation.pressHome before clicking");
-                        waitForSystemUiObject("home").click();
-                    },
-                    event -> true,
-                    () -> "Pressing Home didn't produce any events");
-            mDevice.waitForIdle();
+            log("Hierarchy before clicking home:");
+            dumpViewHierarchy();
+            log(action = "clicking home button from " + getVisibleStateMessage());
+            try (LauncherInstrumentation.Closable c = addContextLayer(action)) {
+                mDevice.waitForIdle();
+                runToState(
+                        waitForSystemUiObject("home")::click,
+                        NORMAL_STATE_ORDINAL,
+                        !hasLauncherObject(WORKSPACE_RES_ID)
+                                && (hasLauncherObject(APPS_RES_ID)
+                                || hasLauncherObject(OVERVIEW_RES_ID)));
+                mDevice.waitForIdle();
+            }
         }
         try (LauncherInstrumentation.Closable c = addContextLayer(
                 "performed action to switch to Home - " + action)) {
@@ -670,6 +704,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)),
@@ -779,12 +827,20 @@
                 + "]";
     }
 
+    void runToState(Runnable command, int expectedState, boolean requireEvent) {
+        if (requireEvent) {
+            runToState(command, expectedState);
+        } else {
+            command.run();
+        }
+    }
+
     void runToState(Runnable command, int expectedState) {
         final List<Integer> actualEvents = new ArrayList<>();
         executeAndWaitForEvent(
                 command,
                 event -> isSwitchToStateEvent(event, expectedState, actualEvents),
-                () -> "Failed to receive an event for the swipe end: expected "
+                () -> "Failed to receive an event for the state change: expected "
                         + TestProtocol.stateOrdinalToString(expectedState)
                         + ", actual: " + eventListToString(actualEvents));
     }
@@ -970,6 +1026,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");
@@ -1029,6 +1095,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);
     }
@@ -1050,4 +1120,104 @@
         }
         return tasks;
     }
+
+    private List<String> getEvents() {
+        final ArrayList<String> events = new ArrayList<>();
+        try {
+            final String logcatTimeParameter =
+                    mTimeBeforeFirstLogEvent != null ? " -t " + mTimeBeforeFirstLogEvent : "";
+            final String logcatEvents = mDevice.executeShellCommand(
+                    "logcat -d --pid=" + getPid() + logcatTimeParameter
+                            + " -s " + TestProtocol.TAPL_EVENTS_TAG);
+            final Matcher matcher = EVENT_LOG_ENTRY.matcher(logcatEvents);
+            while (matcher.find()) {
+                final String eventTime = matcher.group("time");
+                if (eventTime.equals(mTimeBeforeFirstLogEvent)) continue;
+
+                events.add(matcher.group("event"));
+            }
+            return events;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void startRecordingEvents() {
+        Assert.assertTrue("Already recording events", mExpectedEvents == null);
+        mExpectedEvents = new ArrayList<>();
+
+        try {
+            final String lastLogLine =
+                    mDevice.executeShellCommand("logcat -d --pid=" + getPid() + " -t 1");
+            final Matcher matcher = LOG_TIME.matcher(lastLogLine);
+            mTimeBeforeFirstLogEvent = matcher.find() ? matcher.group().replaceAll(" ", "") : null;
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void stopRecordingEvents() {
+        mExpectedEvents = null;
+    }
+
+    Closable eventsCheck() {
+        // Entering events check block.
+        startRecordingEvents();
+
+        return () -> {
+            // Leaving events check block.
+            if (mExpectedEvents == null) {
+                return; // There was a failure. Noo need to report another one.
+            }
+
+            // Wait until Launcher generates expected number of events.
+            final long endTime = SystemClock.uptimeMillis() + WAIT_TIME_MS;
+            while (SystemClock.uptimeMillis() < endTime
+                    && getEvents().size() < mExpectedEvents.size()) {
+                SystemClock.sleep(100);
+            }
+
+            final String message = getEventMismatchMessage();
+            if (message != null) {
+                Assert.fail(formatSystemHealthMessage(
+                        "http://go/tapl : unexpected event sequence: " + message));
+            }
+        };
+    }
+
+    void expectEvent(Pattern expected) {
+        if (mExpectedEvents != null) mExpectedEvents.add(expected);
+    }
+
+    private String getEventMismatchMessage() {
+        if (mExpectedEvents == null) return null;
+
+        try {
+            final List<String> actual = getEvents();
+
+            for (int i = 0; i < mExpectedEvents.size(); ++i) {
+                if (i >= actual.size()) {
+                    return formatEventMismatchMessage("too few actual events", actual, i);
+                }
+                if (!mExpectedEvents.get(i).matcher(actual.get(i)).find()) {
+                    return formatEventMismatchMessage("mismatched event", actual, i);
+                }
+            }
+
+            if (actual.size() > mExpectedEvents.size()) {
+                return formatEventMismatchMessage(
+                        "too many actual events", actual, mExpectedEvents.size());
+            }
+        } finally {
+            stopRecordingEvents();
+        }
+
+        return null;
+    }
+
+    private String formatEventMismatchMessage(String message, List<String> actual, int position) {
+        return message + ", pos=" + position
+                + ", expected=" + mExpectedEvents
+                + ", actual" + actual;
+    }
 }
\ No newline at end of file
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/TestHelpers.java b/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
index e882171..b8791e8 100644
--- a/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
+++ b/tests/tapl/com/android/launcher3/tapl/TestHelpers.java
@@ -151,8 +151,7 @@
                     ? "Current time: " + new Date(System.currentTimeMillis()) + "\n" + errors
                     : null;
         } catch (Exception e) {
-            return "Failed to get system health diags, maybe build your test via .bp instead of "
-                    + ".mk? " + android.util.Log.getStackTraceString(e);
+            return null;
         }
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Widget.java b/tests/tapl/com/android/launcher3/tapl/Widget.java
index 1b6d8c4..e0db16d 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widget.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widget.java
@@ -18,10 +18,15 @@
 
 import androidx.test.uiautomator.UiObject2;
 
+import java.util.regex.Pattern;
+
 /**
  * Widget in workspace or a widget list.
  */
 public final class Widget extends Launchable {
+
+    private static final Pattern LONG_CLICK_EVENT = Pattern.compile("Widgets.onLongClick");
+
     Widget(LauncherInstrumentation launcher, UiObject2 icon) {
         super(launcher, icon);
     }
@@ -30,4 +35,9 @@
     protected String getLongPressIndicator() {
         return "drop_target_bar";
     }
+
+    @Override
+    protected void addExpectedEventsForLongClick() {
+        mLauncher.expectEvent(LONG_CLICK_EVENT);
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 8a53ef1..af7e552 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -22,6 +22,7 @@
 
 import static junit.framework.TestCase.assertTrue;
 
+import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.SystemClock;
@@ -49,6 +50,34 @@
         mHotseat = launcher.waitForLauncherObject("hotseat");
     }
 
+    private static boolean supportsRoundedCornersOnWindows(Resources resources) {
+        return ResourceUtils.getBoolByName(
+                "config_supportsRoundedCornersOnWindows", resources, false);
+    }
+
+    private static float getWindowCornerRadius(Resources resources) {
+        if (!supportsRoundedCornersOnWindows(resources)) {
+            return 0f;
+        }
+
+        // Radius that should be used in case top or bottom aren't defined.
+        float defaultRadius = ResourceUtils.getDimenByName("rounded_corner_radius", resources, 0);
+
+        float topRadius = ResourceUtils.getDimenByName("rounded_corner_radius_top", resources, 0);
+        if (topRadius == 0f) {
+            topRadius = defaultRadius;
+        }
+        float bottomRadius = ResourceUtils.getDimenByName(
+                "rounded_corner_radius_bottom", resources, 0);
+        if (bottomRadius == 0f) {
+            bottomRadius = defaultRadius;
+        }
+
+        // Always use the smallest radius to make sure the rounded corners will
+        // completely cover the display.
+        return Math.min(topRadius, bottomRadius);
+    }
+
     /**
      * Swipes up to All Apps.
      *
@@ -59,13 +88,12 @@
         try (LauncherInstrumentation.Closable c =
                      mLauncher.addContextLayer("want to switch from workspace to all apps")) {
             verifyActiveContainer();
-            final UiObject2 hotseat = mHotseat;
-            final Point start = hotseat.getVisibleCenter();
-            int deviceHeight = mLauncher.getDevice().getDisplayHeight();
-            int bottomGestureMargin = ResourceUtils.getNavbarSize(
+            final int deviceHeight = mLauncher.getDevice().getDisplayHeight();
+            final int bottomGestureMargin = ResourceUtils.getNavbarSize(
                     ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mLauncher.getResources());
-            int displayBottom = deviceHeight - bottomGestureMargin;
-            start.y = displayBottom - 1;
+            final int windowCornerRadius = (int) Math.ceil(getWindowCornerRadius(
+                    mLauncher.getResources()));
+            final int startY = deviceHeight - Math.max(bottomGestureMargin, windowCornerRadius) - 1;
             final int swipeHeight = mLauncher.getTestInfo(
                     TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT).
                     getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
@@ -74,10 +102,10 @@
                             + mLauncher.getTouchSlop());
 
             mLauncher.swipeToState(
-                    start.x,
-                    start.y,
-                    start.x,
-                    start.y - swipeHeight - mLauncher.getTouchSlop(),
+                    0,
+                    startY,
+                    0,
+                    startY - swipeHeight - mLauncher.getTouchSlop(),
                     12,
                     ALL_APPS_STATE_ORDINAL);
 
@@ -155,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