Merge "Removing unused keyboard handling code" into sc-dev
diff --git a/Android.bp b/Android.bp
index b20b307..002f6fe 100644
--- a/Android.bp
+++ b/Android.bp
@@ -12,6 +12,23 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+package {
+    default_applicable_licenses: ["packages_apps_Launcher3_license"],
+}
+
+// Added automatically by a large-scale-change
+// See: http://go/android-license-faq
+license {
+    name: "packages_apps_Launcher3_license",
+    visibility: [":__subpackages__"],
+    license_kinds: [
+        "SPDX-license-identifier-Apache-2.0",
+    ],
+    license_text: [
+        "NOTICE",
+    ],
+}
+
 android_library {
     name: "launcher-aosp-tapl",
     static_libs: [
@@ -24,7 +41,6 @@
     ],
     srcs: [
         "tests/tapl/**/*.java",
-        "src/com/android/launcher3/util/SecureSettingsObserver.java",
         "src/com/android/launcher3/ResourceUtils.java",
         "src/com/android/launcher3/testing/TestProtocol.java",
     ],
@@ -36,17 +52,37 @@
     name: "launcher_log_protos_lite",
     srcs: [
         "protos/*.proto",
+        "protos_overrides/*.proto",
     ],
     sdk_version: "current",
     proto: {
         type: "lite",
         local_include_dirs:[
             "protos",
+            "protos_overrides",
         ],
     },
     static_libs: ["libprotobuf-java-lite"],
 }
 
+java_library_static {
+    name: "launcher_quickstep_log_protos_lite",
+    srcs: [
+        "quickstep/protos_overrides/*.proto",
+    ],
+    sdk_version: "current",
+    proto: {
+        type: "lite",
+        local_include_dirs:[
+            "quickstep/protos_overrides",
+        ],
+    },
+    static_libs: [
+      "libprotobuf-java-lite",
+      "launcher_log_protos_lite"
+      ],
+}
+
 java_library {
     name: "LauncherPluginLib",
 
@@ -57,3 +93,93 @@
     sdk_version: "current",
     min_sdk_version: "28",
 }
+
+//
+// Build rule for Launcher3 dependencies lib.
+//
+android_library {
+    name: "Launcher3CommonDepsLib",
+    static_libs: [
+        "androidx.recyclerview_recyclerview",
+        "androidx.dynamicanimation_dynamicanimation",
+        "androidx.preference_preference",
+        "androidx.slice_slice-view",
+        "iconloader_base",
+        "LauncherPluginLib",
+        "launcher_quickstep_log_protos_lite"
+    ],
+    srcs: [
+        "src_build_config/**/*.java",
+    ],
+    resource_dirs: ["res"],
+    optimize: {
+        enabled: false,
+    },
+    sdk_version: "current",
+    min_sdk_version: "26",
+    manifest: "AndroidManifest-common.xml",
+}
+
+//
+// Build rule for Launcher3 app.
+//
+android_app {
+    name: "Launcher3",
+
+    static_libs: [
+        "Launcher3CommonDepsLib",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src_shortcuts_overrides/**/*.java",
+        "src_ui_overrides/**/*.java",
+        "ext_tests/src/**/*.java",
+    ],
+    resource_dirs: [
+        "ext_tests/res",
+    ],
+    optimize: {
+        proguard_flags_files: ["proguard.flags"],
+        // Proguard is disable for testing. Derivarive prjects to keep proguard enabled
+        enabled: false,
+    },
+
+    sdk_version: "current",
+    min_sdk_version: "26",
+    target_sdk_version: "29",
+    privileged: true,
+    system_ext_specific: true,
+
+    overrides: [
+        "Home",
+        "Launcher2",
+    ],
+    required: ["privapp_whitelist_com.android.launcher3"],
+
+    jacoco: {
+        include_filter: ["com.android.launcher3.**"],
+    },
+    additional_manifests: [
+        "AndroidManifest-common.xml",
+    ],
+}
+
+//
+// Launcher Robolectric test target.
+//
+java_library {
+    name: "Launcher3TestCommon",
+    libs: [
+        "Launcher3CommonDepsLib",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src_shortcuts_overrides/**/*.java",
+        "src_ui_overrides/**/*.java",
+        "ext_tests/src/**/*.java",
+        "tests/src_common/**/*.java",
+    ],
+    target_sdk_version: "29",
+    sdk_version: "current",
+    min_sdk_version: "26",
+}
diff --git a/Android.mk b/Android.mk
index ed42039..89870a6 100644
--- a/Android.mk
+++ b/Android.mk
@@ -17,76 +17,6 @@
 LOCAL_PATH := $(call my-dir)
 
 #
-# Build rule for Launcher3 dependencies lib.
-#
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT2_ONLY := true
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_STATIC_ANDROID_LIBRARIES := \
-    androidx.recyclerview_recyclerview \
-    androidx.dynamicanimation_dynamicanimation \
-    androidx.preference_preference \
-    androidx.slice_slice-view \
-    iconloader_base
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
-    LauncherPluginLib \
-    launcher_log_protos_lite \
-    search_ui
-
-LOCAL_SRC_FILES := \
-    $(call all-java-files-under, src_build_config) \
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
-LOCAL_PROGUARD_ENABLED := disabled
-
-LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 26
-LOCAL_MODULE := Launcher3CommonDepsLib
-LOCAL_PRIVILEGED_MODULE := true
-LOCAL_MANIFEST_FILE := AndroidManifest-common.xml
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-#
-# Build rule for Launcher3 app.
-#
-include $(CLEAR_VARS)
-LOCAL_USE_AAPT2 := true
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3CommonDepsLib
-
-LOCAL_SRC_FILES := \
-    $(call all-java-files-under, src) \
-    $(call all-java-files-under, src_shortcuts_overrides) \
-    $(call all-java-files-under, src_ui_overrides) \
-    $(call all-java-files-under, ext_tests/src)
-    
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/ext_tests/res
-
-LOCAL_PROGUARD_FLAG_FILES := proguard.flags
-# Proguard is disable for testing. Derivarive prjects to keep proguard enabled
-LOCAL_PROGUARD_ENABLED := disabled
-
-LOCAL_SDK_VERSION := current
-LOCAL_MIN_SDK_VERSION := 26
-LOCAL_PACKAGE_NAME := Launcher3
-LOCAL_PRIVILEGED_MODULE := true
-LOCAL_SYSTEM_EXT_MODULE := true
-LOCAL_OVERRIDES_PACKAGES := Home Launcher2
-LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
-
-LOCAL_FULL_LIBS_MANIFEST_FILES := $(LOCAL_PATH)/AndroidManifest-common.xml
-
-LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.launcher3.*
-
-include $(BUILD_PACKAGE)
-
-#
 # Build rule for Launcher3 Go app for Android Go devices.
 #
 include $(CLEAR_VARS)
@@ -129,8 +59,7 @@
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
     SystemUI-statsd \
-    SystemUISharedLib \
-    launcher_log_protos_lite
+    SystemUISharedLib
 ifneq (,$(wildcard frameworks/base))
   LOCAL_PRIVATE_PLATFORM_APIS := true
 else
@@ -138,6 +67,9 @@
   LOCAL_MIN_SDK_VERSION := 26
 endif
 LOCAL_MODULE := Launcher3QuickStepLib
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+LOCAL_NOTICE_FILE := $(LOCAL_PATH)/NOTICE
 LOCAL_PRIVILEGED_MODULE := true
 LOCAL_STATIC_ANDROID_LIBRARIES := Launcher3CommonDepsLib
 
@@ -146,7 +78,9 @@
     $(call all-java-files-under, quickstep/src) \
     $(call all-java-files-under, src_shortcuts_overrides)
 
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/quickstep/res
+LOCAL_RESOURCE_DIR := \
+    $(LOCAL_PATH)/quickstep/res \
+    $(LOCAL_PATH)/quickstep/overview_ui_overrides/res
 LOCAL_PROGUARD_ENABLED := disabled
 
 
@@ -175,7 +109,9 @@
 LOCAL_OVERRIDES_PACKAGES := Home Launcher2 Launcher3
 LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.launcher3
 
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/quickstep/res
+LOCAL_RESOURCE_DIR := \
+    $(LOCAL_PATH)/quickstep/res \
+    $(LOCAL_PATH)/quickstep/overview_ui_overrides/res
 
 LOCAL_FULL_LIBS_MANIFEST_FILES := \
     $(LOCAL_PATH)/quickstep/AndroidManifest-launcher.xml \
@@ -196,8 +132,7 @@
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
     SystemUI-statsd \
-    SystemUISharedLib \
-    launcher_log_protos_lite
+    SystemUISharedLib
 ifneq (,$(wildcard frameworks/base))
   LOCAL_PRIVATE_PLATFORM_APIS := true
 else
@@ -215,7 +150,8 @@
 LOCAL_RESOURCE_DIR := \
     $(LOCAL_PATH)/quickstep/res \
     $(LOCAL_PATH)/go/res \
-    $(LOCAL_PATH)/go/quickstep/res
+    $(LOCAL_PATH)/go/quickstep/res \
+    $(LOCAL_PATH)/go/quickstep/overview_ui_overrides/res
 
 LOCAL_PROGUARD_FLAG_FILES := proguard.flags
 LOCAL_PROGUARD_ENABLED := full
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 56a2595..1fad72d 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -49,7 +49,7 @@
             android:stateNotNeeded="true"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
diff --git a/build.gradle b/build.gradle
index 28a05d5..a7eef13 100644
--- a/build.gradle
+++ b/build.gradle
@@ -81,7 +81,7 @@
             java.srcDirs = ['src', 'src_plugins']
             manifest.srcFile 'AndroidManifest-common.xml'
             proto {
-                srcDir 'protos/'
+                srcDirs = ['protos/', 'protos_overrides/']
             }
         }
 
@@ -181,4 +181,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/go/AndroidManifest-launcher.xml b/go/AndroidManifest-launcher.xml
index 2223036..d2575b6 100644
--- a/go/AndroidManifest-launcher.xml
+++ b/go/AndroidManifest-launcher.xml
@@ -49,7 +49,7 @@
             android:stateNotNeeded="true"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|uiMode"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
diff --git a/go/quickstep/overview_ui_overrides/res/layout/overview_actions_container.xml b/go/quickstep/overview_ui_overrides/res/layout/overview_actions_container.xml
new file mode 100644
index 0000000..b438da3
--- /dev/null
+++ b/go/quickstep/overview_ui_overrides/res/layout/overview_actions_container.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2021 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.GoOverviewActionsView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/overview_actions_height"
+    android:layout_gravity="center_horizontal|bottom">
+
+    <LinearLayout
+        android:id="@+id/action_buttons"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:orientation="horizontal">
+
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="1" />
+
+        <Button
+            android:id="@+id/action_listen"
+            style="@style/OverviewActionButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:drawableTop="@drawable/ic_listen"
+            android:drawablePadding="1dp"
+            android:text="@string/action_listen"
+            android:theme="@style/ThemeControlHighlightWorkspaceColor" />
+
+        <Button
+            android:id="@+id/action_translate"
+            style="@style/OverviewActionButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:drawableTop="@drawable/ic_translate"
+            android:drawablePadding="1dp"
+            android:text="@string/action_translate"
+            android:theme="@style/ThemeControlHighlightWorkspaceColor" />
+
+        <Button
+            android:id="@+id/action_search"
+            style="@style/OverviewActionButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:drawableTop="@drawable/ic_search"
+            android:drawablePadding="1dp"
+            android:text="@string/action_search"
+            android:theme="@style/ThemeControlHighlightWorkspaceColor" />
+
+        <Button
+            android:id="@+id/action_screenshot"
+            style="@style/OverviewActionButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:drawableTop="@drawable/ic_screenshot"
+            android:drawablePadding="1dp"
+            android:text="@string/action_screenshot"
+            android:theme="@style/ThemeControlHighlightWorkspaceColor" />
+
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="1" />
+
+        <Button
+            android:id="@+id/action_share"
+            style="@style/OverviewActionButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:drawableStart="@drawable/ic_share"
+            android:text="@string/action_share"
+            android:theme="@style/ThemeControlHighlightWorkspaceColor"
+            android:visibility="gone" />
+
+        <Space
+            android:id="@+id/oav_three_button_space"
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="1"
+            android:visibility="gone" />
+    </LinearLayout>
+
+</com.android.quickstep.views.GoOverviewActionsView>
\ No newline at end of file
diff --git a/go/quickstep/overview_ui_overrides/res/values/config.xml b/go/quickstep/overview_ui_overrides/res/values/config.xml
new file mode 100644
index 0000000..ec21a01
--- /dev/null
+++ b/go/quickstep/overview_ui_overrides/res/values/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright (C) 2021 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>
+    <string name="task_overlay_factory_class" translatable="false">
+        com.android.quickstep.TaskOverlayFactoryGo</string>
+</resources>
\ No newline at end of file
diff --git a/go/quickstep/res/drawable/ic_listen.xml b/go/quickstep/res/drawable/ic_listen.xml
new file mode 100644
index 0000000..a8e6c93
--- /dev/null
+++ b/go/quickstep/res/drawable/ic_listen.xml
@@ -0,0 +1,32 @@
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="28dp"
+    android:height="28dp"
+    android:viewportWidth="28"
+    android:viewportHeight="28">
+  <path
+      android:pathData="M10.5,15.17c2.58,0 4.67,-2.09 4.67,-4.67s-2.09,-4.67 -4.67,-4.67c-2.58,0 -4.67,2.09 -4.67,4.67S7.92,15.17 10.5,15.17zM10.5,8.17c1.28,0 2.33,1.05 2.33,2.33s-1.05,2.33 -2.33,2.33c-1.28,0 -2.33,-1.05 -2.33,-2.33S9.22,8.17 10.5,8.17z"
+      android:fillColor="#4285F4"/>
+  <path
+      android:pathData="M10.5,17.5c-3.11,0 -9.33,1.56 -9.33,4.67v2.33h18.67v-2.33C19.83,19.06 13.62,17.5 10.5,17.5zM3.5,22.17c0.26,-0.84 3.86,-2.33 7,-2.33c3.15,0 6.77,1.5 7,2.33H3.5z"
+      android:fillColor="#4285F4"/>
+  <path
+      android:pathData="M25.67,10.5c0,0.36 -0.02,0.71 -0.05,1.05c-0.01,0.15 -0.03,0.29 -0.05,0.43c-0.02,0.18 -0.05,0.36 -0.08,0.54c-0.04,0.2 -0.07,0.39 -0.12,0.58c-0.01,0.06 -0.03,0.11 -0.04,0.17c-0.59,2.34 -1.81,4.01 -2.52,4.82c-0.09,0.1 -0.18,0.2 -0.28,0.3c-0.17,0.18 -0.27,0.27 -0.27,0.27l-1.65,-1.63c1.34,-1.33 2.27,-3.07 2.6,-5.01c0.01,-0.08 0.02,-0.16 0.04,-0.24c0.06,-0.42 0.1,-0.85 0.1,-1.29c0,-0.44 -0.04,-0.88 -0.1,-1.3c-0.01,-0.06 -0.02,-0.13 -0.03,-0.19c-0.32,-1.95 -1.25,-3.7 -2.6,-5.04l1.65,-1.63c0,0 0.11,0.1 0.27,0.27c0.09,0.1 0.19,0.2 0.28,0.3c0.71,0.82 1.93,2.48 2.52,4.82c0.01,0.06 0.03,0.11 0.04,0.17c0.04,0.19 0.08,0.38 0.12,0.58c0.03,0.18 0.06,0.36 0.08,0.54c0.02,0.14 0.04,0.28 0.05,0.43C25.65,9.79 25.67,10.14 25.67,10.5z"
+      android:fillColor="#EA4335"/>
+  <path
+      android:pathData="M20.61,8.4C20.85,9.06 21,9.76 21,10.5s-0.15,1.44 -0.39,2.1c-0.28,0.77 -0.71,1.46 -1.25,2.05l-1.66,-1.64c0.56,-0.63 0.91,-1.44 0.95,-2.34c0,-0.06 0.02,-0.11 0.02,-0.17s-0.01,-0.11 -0.02,-0.17c-0.04,-0.9 -0.39,-1.71 -0.95,-2.34l1.66,-1.64C19.9,6.94 20.32,7.63 20.61,8.4z"
+      android:fillColor="#FBBC04"/>
+</vector>
diff --git a/go/quickstep/res/drawable/ic_search.xml b/go/quickstep/res/drawable/ic_search.xml
new file mode 100644
index 0000000..4307330
--- /dev/null
+++ b/go/quickstep/res/drawable/ic_search.xml
@@ -0,0 +1,32 @@
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="28dp"
+    android:height="28dp"
+    android:viewportWidth="28"
+    android:viewportHeight="28">
+  <path
+      android:pathData="M24.5,22.75l-6.84,-6.84c1,-1.35 1.59,-3.02 1.59,-4.83h-2.33c0,3.22 -2.62,5.83 -5.83,5.83v2.33c1.81,0 3.47,-0.6 4.83,-1.59l6.84,6.84L24.5,22.75z"
+      android:fillColor="#4285F4"/>
+  <path
+      android:pathData="M11.08,2.92v2.33c3.22,0 5.83,2.62 5.83,5.83h2.33C19.25,6.57 15.59,2.92 11.08,2.92z"
+      android:fillColor="#34A853"/>
+  <path
+      android:pathData="M5.25,11.08H2.92c0,4.51 3.66,8.17 8.17,8.17v-2.33C7.87,16.92 5.25,14.3 5.25,11.08z"
+      android:fillColor="#EA4335"/>
+  <path
+      android:pathData="M2.92,11.08h2.33c0,-3.22 2.62,-5.83 5.83,-5.83V2.92C6.57,2.92 2.92,6.57 2.92,11.08z"
+      android:fillColor="#FBBC04"/>
+</vector>
diff --git a/go/quickstep/res/drawable/ic_translate.xml b/go/quickstep/res/drawable/ic_translate.xml
new file mode 100644
index 0000000..1247807
--- /dev/null
+++ b/go/quickstep/res/drawable/ic_translate.xml
@@ -0,0 +1,32 @@
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="28dp"
+    android:height="28dp"
+    android:viewportWidth="28"
+    android:viewportHeight="28">
+  <path
+      android:pathData="M12.28,15.19l-0.07,-0.05c-0.61,-0.49 -1.15,-1.05 -1.65,-1.63c-1.05,-1.22 -1.88,-2.63 -2.39,-4.17H5.83c0.54,2.17 1.58,4.16 3.01,5.85l-5.93,5.23l1.75,1.75l5.91,-5.26c0.05,0.04 0.1,0.09 0.15,0.13l3.42,2.79l1.02,-2.33L12.28,15.19z"
+      android:fillColor="#FBBC04"/>
+  <path
+      android:pathData="M21.58,11.67h-2.33l-5.25,14h2.33l1.31,-3.5h5.54l1.32,3.5h2.33L21.58,11.67zM18.53,19.83l1.89,-5.05l1.89,5.05H18.53z"
+      android:fillColor="#4285F4"/>
+  <path
+      android:pathData="M11.67,2.33l-2.34,0l0,2.34l-8.16,0l0,2.33l10.5,0l0,-2.33z"
+      android:fillColor="#EA4335"/>
+  <path
+      android:pathData="M11.67,4.67V7H14c-0.61,2.42 -1.79,4.65 -3.44,6.5c0.5,0.59 1.04,1.15 1.65,1.63l0.07,0.05c2.03,-2.32 3.44,-5.14 4.05,-8.19h3.5V4.67H11.67z"
+      android:fillColor="#34A853"/>
+</vector>
diff --git a/go/quickstep/res/values/config.xml b/go/quickstep/res/values/config.xml
index f376774..9dca137 100644
--- a/go/quickstep/res/values/config.xml
+++ b/go/quickstep/res/values/config.xml
@@ -14,5 +14,11 @@
      limitations under the License.
 -->
 <resources>
+    <!-- The component to receive app sharing Intents -->
     <string name="app_sharing_component" translatable="false"/>
+    <!-- The package to receive Listen, Translate, and Search Intents -->
+    <string name="niu_actions_package" translatable="false"/>
+
+    <!-- Feature Flags -->
+    <bool name="enable_niu_actions">false</bool>
 </resources>
\ No newline at end of file
diff --git a/go/quickstep/res/values/strings.xml b/go/quickstep/res/values/strings.xml
index fdd8397..71e2f3a 100644
--- a/go/quickstep/res/values/strings.xml
+++ b/go/quickstep/res/values/strings.xml
@@ -3,4 +3,12 @@
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <!-- Label for app share drop target. [CHAR_LIMIT=20] -->
     <string name="app_share_drop_target_label">Share App</string>
+
+    <!-- ******* Overview ******* -->
+    <!-- Label for a button that lets the user listen to the content of the current app. [CHAR_LIMIT=40] -->
+    <string name="action_listen">Listen</string>
+    <!-- Label for a button that translates a screenshot of the current app. [CHAR_LIMIT=40] -->
+    <string name="action_translate">Translate</string>
+    <!-- Label for a button that triggers Search on a screenshot of the current app. [CHAR_LIMIT=40] -->
+    <string name="action_search">Search</string>
 </resources>
diff --git a/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java b/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
new file mode 100644
index 0000000..b102a39
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/TaskOverlayFactoryGo.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2021 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.views.OverviewActionsView.DISABLED_NO_THUMBNAIL;
+import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Matrix;
+import android.os.SystemClock;
+import android.text.TextUtils;
+
+import com.android.launcher3.R;
+import com.android.quickstep.views.OverviewActionsView;
+import com.android.quickstep.views.TaskThumbnailView;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+/**
+ * Go-specific extension of the factory class that adds an overlay to TaskView
+ */
+public final class TaskOverlayFactoryGo extends TaskOverlayFactory {
+    public static final String ACTION_LISTEN = "com.android.quickstep.ACTION_LISTEN";
+    public static final String ACTION_TRANSLATE = "com.android.quickstep.ACTION_TRANSLATE";
+    public static final String ACTION_SEARCH = "com.android.quickstep.ACTION_SEARCH";
+    public static final String ELAPSED_NANOS = "niu_actions_elapsed_realtime_nanos";
+
+    // Empty constructor required for ResourceBasedOverride
+    public TaskOverlayFactoryGo(Context context) {}
+
+    /**
+     * Create a new overlay instance for the given View
+     */
+    public TaskOverlayGo createOverlay(TaskThumbnailView thumbnailView) {
+        return new TaskOverlayGo(thumbnailView);
+    }
+
+    /**
+     * Overlay on each task handling Overview Action Buttons.
+     * @param <T> The type of View in which the overlay will be placed
+     */
+    public static final class TaskOverlayGo<T extends OverviewActionsView> extends TaskOverlay {
+
+        private String mPackageName;
+
+        private TaskOverlayGo(TaskThumbnailView taskThumbnailView) {
+            super(taskThumbnailView);
+        }
+
+        /**
+         * Called when the current task is interactive for the user
+         */
+        @Override
+        public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
+                boolean rotated) {
+            getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null);
+            mPackageName =
+                    mApplicationContext.getResources().getString(R.string.niu_actions_package);
+
+            if (thumbnail == null || TextUtils.isEmpty(mPackageName)) {
+                return;
+            }
+
+            getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
+            boolean isAllowedByPolicy = thumbnail.isRealSnapshot;
+            getActionsView().setCallbacks(new OverlayUICallbacksGoImpl(isAllowedByPolicy, task));
+        }
+
+        private void sendNIUIntent(String actionType) {
+            Intent intent = createNIUIntent(actionType);
+            mImageApi.shareAsDataWithExplicitIntent(/* crop */ null, intent);
+        }
+
+        private Intent createNIUIntent(String actionType) {
+            return new Intent(actionType)
+                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)
+                    .addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
+                    .setPackage(mPackageName)
+                    .putExtra(ELAPSED_NANOS, SystemClock.elapsedRealtimeNanos());
+        }
+
+        protected class OverlayUICallbacksGoImpl extends OverlayUICallbacksImpl
+                implements OverlayUICallbacksGo {
+            public OverlayUICallbacksGoImpl(boolean isAllowedByPolicy, Task task) {
+                super(isAllowedByPolicy, task);
+            }
+
+            @SuppressLint("NewApi")
+            public void onListen() {
+                if (mIsAllowedByPolicy) {
+                    sendNIUIntent(ACTION_LISTEN);
+                } else {
+                    showBlockedByPolicyMessage();
+                }
+            }
+
+            @SuppressLint("NewApi")
+            public void onTranslate() {
+                if (mIsAllowedByPolicy) {
+                    sendNIUIntent(ACTION_TRANSLATE);
+                } else {
+                    showBlockedByPolicyMessage();
+                }
+            }
+
+            @SuppressLint("NewApi")
+            public void onSearch() {
+                if (mIsAllowedByPolicy) {
+                    sendNIUIntent(ACTION_SEARCH);
+                } else {
+                    showBlockedByPolicyMessage();
+                }
+            }
+        }
+    }
+
+    /**
+     * Callbacks the Ui can generate. This is the only way for a Ui to call methods on the
+     * controller.
+     */
+    public interface OverlayUICallbacksGo extends OverlayUICallbacks {
+        /** User has requested to listen to the current content read aloud */
+        void onListen();
+
+        /** User has requested a translation of the current content */
+        void onTranslate();
+
+        /** User has requested a visual search of the current content */
+        void onSearch();
+    }
+}
diff --git a/go/quickstep/src/com/android/quickstep/views/GoOverviewActionsView.java b/go/quickstep/src/com/android/quickstep/views/GoOverviewActionsView.java
new file mode 100644
index 0000000..9997d16
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/GoOverviewActionsView.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2021 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.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+import com.android.quickstep.TaskOverlayFactoryGo.OverlayUICallbacksGo;
+
+/**
+ * View for showing Go-specific action buttons in Overview
+ */
+public final class GoOverviewActionsView extends OverviewActionsView<OverlayUICallbacksGo> {
+    public GoOverviewActionsView(Context context) {
+        this(context, null);
+    }
+
+    public GoOverviewActionsView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public GoOverviewActionsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        if (getResources().getBoolean(R.bool.enable_niu_actions)) {
+            findViewById(R.id.action_listen).setOnClickListener(this);
+            findViewById(R.id.action_translate).setOnClickListener(this);
+            findViewById(R.id.action_search).setOnClickListener(this);
+        } else {
+            findViewById(R.id.action_listen).setVisibility(View.GONE);
+            findViewById(R.id.action_translate).setVisibility(View.GONE);
+            findViewById(R.id.action_search).setVisibility(View.GONE);
+        }
+    }
+
+    @Override
+    public void onClick(View view) {
+        super.onClick(view);
+
+        if (mCallbacks == null) {
+            return;
+        }
+        int id = view.getId();
+        if (id == R.id.action_listen) {
+            mCallbacks.onListen();
+        } else if (id == R.id.action_translate) {
+            mCallbacks.onTranslate();
+        } else if (id == R.id.action_search) {
+            mCallbacks.onSearch();
+        }
+    }
+}
diff --git a/go/src/com/android/launcher3/model/WidgetsModel.java b/go/src/com/android/launcher3/model/WidgetsModel.java
index 89b3831..cc5e1cb 100644
--- a/go/src/com/android/launcher3/model/WidgetsModel.java
+++ b/go/src/com/android/launcher3/model/WidgetsModel.java
@@ -25,11 +25,12 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.icons.ComponentWithLabelAndIcon;
 import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -43,20 +44,25 @@
     public static final boolean GO_DISABLE_WIDGETS = true;
     public static final boolean GO_DISABLE_NOTIFICATION_DOTS = true;
 
-    private static final ArrayList<WidgetListRowEntry> EMPTY_WIDGET_LIST = new ArrayList<>();
+    private static final ArrayList<WidgetsListBaseEntry> EMPTY_WIDGET_LIST = new ArrayList<>();
 
     /**
-     * Returns a list of {@link WidgetListRowEntry}. All {@link WidgetItem} in a single row
-     * are sorted (based on label and user), but the overall list of {@link WidgetListRowEntry}s
-     * is not sorted. This list is sorted at the UI when using
-     * {@link com.android.launcher3.widget.WidgetsDiffReporter}
+     * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row are
+     * sorted (based on label and user), but the overall list of {@link WidgetsListBaseEntry}s is
+     * not sorted. This list is sorted at the UI when using
+     * {@link com.android.launcher3.widget.picker.WidgetsDiffReporter}
      *
-     * @see com.android.launcher3.widget.WidgetsListAdapter#setWidgets(ArrayList)
+     * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
      */
-    public synchronized ArrayList<WidgetListRowEntry> getWidgetsList(Context context) {
+    public synchronized ArrayList<WidgetsListBaseEntry> getWidgetsListForPicker(Context context) {
         return EMPTY_WIDGET_LIST;
     }
 
+    /** Returns a mapping of packages to their widgets without static shortcuts. */
+    public synchronized Map<PackageUserKey, List<WidgetItem>> getAllWidgetsWithoutShortcuts() {
+        return Map.of();
+    }
+
     /**
      * @param packageUser If null, all widgets and shortcuts are updated and returned, otherwise
      *                    only widgets and shortcuts associated with the package/user are.
diff --git a/protos/launcher_atom.proto b/protos/launcher_atom.proto
index cd229ae..fe81b4c 100644
--- a/protos/launcher_atom.proto
+++ b/protos/launcher_atom.proto
@@ -18,6 +18,8 @@
 option java_package = "com.android.launcher3.logger";
 option java_outer_classname = "LauncherAtom";
 
+import "launcher_atom_extension.proto";
+
 //
 // ItemInfos
 message ItemInfo {
@@ -27,6 +29,8 @@
     Shortcut shortcut = 3;
     Widget widget = 4;
     FolderIcon folder_icon = 9;
+    Slice slice = 10;
+    SearchActionItem search_action_item = 11;
   }
   // When used for launch event, stores the global predictive rank
   optional int32 rank = 5;
@@ -55,6 +59,7 @@
     SettingsContainer settings_container = 9;
     PredictedHotseatContainer predicted_hotseat_container = 10;
     TaskSwitcherContainer task_switcher_container = 11;
+    ExtendedContainers extended_containers = 20;
   }
 }
 
@@ -166,6 +171,17 @@
   optional string label_info = 4;
 }
 
+// Contains Slice details for logging.
+message Slice{
+  optional string uri = 1;
+}
+
+// Represents SearchAction with in launcher
+message SearchActionItem{
+  optional string package_name = 1;
+  optional string title = 2;
+}
+
 //////////////////////////////////////////////
 // Containers
 
diff --git a/protos_overrides/launcher_atom_extension.proto b/protos_overrides/launcher_atom_extension.proto
new file mode 100644
index 0000000..a07daf8
--- /dev/null
+++ b/protos_overrides/launcher_atom_extension.proto
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+syntax = "proto2";
+
+option java_package = "com.android.launcher3.logger";
+option java_outer_classname = "LauncherAtomExtensions";
+
+
+// This proto file contains placeholder messages that can be overridden by
+// other Launcher variants.
+// Variants could have its own launcher_atom_extension.proto file(should have
+// same messages declared here but with different implementation); when building
+// variant's apk launcher_atom.proto will reference variant's extension file,
+// essentially overriding this file.
+
+
+// Wrapper message for additional containers used in variants.
+message ExtendedContainers {}
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
index 53910e3..7fe9b08 100644
--- a/quickstep/AndroidManifest-launcher.xml
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -49,7 +49,7 @@
             android:stateNotNeeded="true"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index d95cc01..5e5cf73 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -31,8 +31,18 @@
     <uses-permission android:name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS"/>
     <uses-permission android:name="android.permission.VIBRATE"/>
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+    <uses-permission android:name="android.permission.START_TASKS_FROM_RECENTS"/>
+    <uses-permission android:name="android.permission.REMOVE_TASKS"/>
+    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/>
+    <uses-permission android:name="android.permission.STATUS_BAR"/>
+    <uses-permission android:name="android.permission.STOP_APP_SWITCHES"/>
+    <uses-permission android:name="android.permission.SET_ORIENTATION"/>
+    <uses-permission android:name="android.permission.READ_FRAME_BUFFER"/>
+    <uses-permission android:name="android.permission.MANAGE_ACCESSIBILITY"/>
+    <uses-permission android:name="android.permission.MONITOR_INPUT"/>
+
     <uses-permission android:name="${packageName}.permission.HOTSEAT_EDU" />
-    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
+    <uses-permission android:name="android.permission.SYSTEM_APPLICATION_OVERLAY" />
 
     <application android:backupAgent="com.android.launcher3.LauncherBackupAgent"
          android:fullBackupOnly="true"
@@ -61,7 +71,7 @@
              android:stateNotNeeded="true"
              android:theme="@style/LauncherTheme"
              android:screenOrientation="unspecified"
-             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout"
              android:resizeableActivity="true"
              android:resumeWhilePausing="true"
              android:taskAffinity=""/>
diff --git a/quickstep/res/layout/overview_actions_container.xml b/quickstep/overview_ui_overrides/res/layout/overview_actions_container.xml
similarity index 100%
rename from quickstep/res/layout/overview_actions_container.xml
rename to quickstep/overview_ui_overrides/res/layout/overview_actions_container.xml
diff --git a/quickstep/res/layout/search_result_thumbnail.xml b/quickstep/overview_ui_overrides/res/values/config.xml
similarity index 64%
copy from quickstep/res/layout/search_result_thumbnail.xml
copy to quickstep/overview_ui_overrides/res/values/config.xml
index 5062b76..0f09439 100644
--- a/quickstep/res/layout/search_result_thumbnail.xml
+++ b/quickstep/overview_ui_overrides/res/values/config.xml
@@ -1,11 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
+<!-- Copyright (C) 2021 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
+        http://www.apache.org/licenses/LICENSE-2.0
 
      Unless required by applicable law or agreed to in writing, software
      distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,7 +13,6 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.search.SearchResultThumbnailView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="125dp"
-    android:layout_height="125dp"/>
\ No newline at end of file
+<resources>
+    <string name="task_overlay_factory_class" translatable="false"/>
+</resources>
\ No newline at end of file
diff --git a/quickstep/protos_overrides/launcher_atom_extension.proto b/quickstep/protos_overrides/launcher_atom_extension.proto
new file mode 100644
index 0000000..6253b41
--- /dev/null
+++ b/quickstep/protos_overrides/launcher_atom_extension.proto
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+syntax = "proto2";
+
+option java_package = "com.android.launcher3.logger";
+option java_outer_classname = "LauncherAtomExtensions";
+
+
+// Wrapper message for containers used at the quickstep level.
+// Message name should match with launcher_atom_extension.proto message at
+// the AOSP level.
+message ExtendedContainers {
+
+  oneof Container{
+    DeviceSearchResultContainer device_search_result_container = 1;
+  }
+}
+
+// Represents on-device search result container.
+message DeviceSearchResultContainer{
+  optional int32 query_length = 1;
+}
diff --git a/quickstep/res/drawable/chip_scrim_gradient.xml b/quickstep/res/drawable/chip_scrim_gradient.xml
deleted file mode 100644
index 5a2dfb7..0000000
--- a/quickstep/res/drawable/chip_scrim_gradient.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
-    <gradient
-        android:angle="90"
-        android:endColor="@android:color/transparent"
-        android:startColor="@color/chip_scrim_start_color"
-        android:type="linear" />
-</shape>
\ No newline at end of file
diff --git a/quickstep/res/layout/fallback_search_input.xml b/quickstep/res/layout/fallback_search_input.xml
deleted file mode 100644
index 0fb2924..0000000
--- a/quickstep/res/layout/fallback_search_input.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2020 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.launcher3.search.FallbackSearchInputView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:layout_gravity="center_vertical"
-    android:layout_marginLeft="48dp"
-    android:layout_marginRight="48dp"
-    android:background="@android:color/transparent"
-    android:focusableInTouchMode="true"
-    android:gravity="start|center_vertical"
-    android:inputType="textNoSuggestions"
-    android:imeOptions="actionSearch|flagNoExtractUi"
-    android:maxLines="1"
-    android:privateImeOptions="bc_search"
-    android:scrollHorizontally="true"
-    android:singleLine="true"
-    android:textColor="?android:attr/textColorSecondary"
-    android:textColorHint="?android:attr/textColorTertiary"
-    android:textSize="16sp" />
\ No newline at end of file
diff --git a/quickstep/res/layout/search_edu_view.xml b/quickstep/res/layout/search_edu_view.xml
deleted file mode 100644
index d89f5c7..0000000
--- a/quickstep/res/layout/search_edu_view.xml
+++ /dev/null
@@ -1,73 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2008 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.launcher3.search.DeviceSearchEdu
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:layout_gravity="center_horizontal"
-    android:orientation="vertical">
-
-
-    <FrameLayout
-        android:layout_height="wrap_content"
-        android:id="@+id/search_box_wrapper"
-        android:layout_width="match_parent">
-
-        <include
-            layout="@layout/fallback_search_input"
-            android:id="@+id/mock_search_box" />
-    </FrameLayout>
-
-    <LinearLayout
-        android:layout_height="wrap_content"
-        android:id="@+id/edu_wrapper"
-        android:padding="24dp"
-        android:layout_marginTop="40dp"
-        android:orientation="vertical"
-        android:layout_width="match_parent">
-
-        <TextView
-            style="@style/TextHeadline"
-            android:layout_width="match_parent"
-            android:gravity="center"
-            android:textSize="24sp"
-            android:textColor="?android:attr/textColorPrimary"
-            android:layout_height="wrap_content"
-            android:text="@string/search_edu_primary" />
-
-        <TextView
-            style="@style/TextHeadline"
-            android:layout_width="match_parent"
-            android:gravity="center"
-            android:textSize="18sp"
-            android:layout_marginTop="30dp"
-            android:textColor="?android:attr/textColorPrimary"
-            android:layout_height="wrap_content"
-            android:text="@string/search_edu_secondary" />
-
-        <Button
-            android:id="@+id/dismiss_edu"
-            android:layout_width="wrap_content"
-            android:layout_marginTop="@dimen/dynamic_grid_edge_margin"
-            android:background="?android:attr/selectableItemBackground"
-            android:layout_height="wrap_content"
-            android:textColor="?android:attr/textColorPrimary"
-            android:gravity="center"
-            android:layout_gravity="center"
-            android:text="@string/search_edu_dismiss" />
-
-    </LinearLayout>
-
-</com.android.launcher3.search.DeviceSearchEdu>
\ No newline at end of file
diff --git a/quickstep/res/layout/search_result_icon.xml b/quickstep/res/layout/search_result_icon.xml
deleted file mode 100644
index e1b6dfd..0000000
--- a/quickstep/res/layout/search_result_icon.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2008 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<com.android.launcher3.search.SearchResultIcon xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    style="@style/BaseIcon.AllApps"
-    launcher:iconDisplay="all_apps"
-    launcher:centerVertically="true" />
-
diff --git a/quickstep/res/layout/search_result_icon_row.xml b/quickstep/res/layout/search_result_icon_row.xml
deleted file mode 100644
index 2119fc3..0000000
--- a/quickstep/res/layout/search_result_icon_row.xml
+++ /dev/null
@@ -1,75 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2020 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.launcher3.search.SearchResultIconRow xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    android:orientation="horizontal"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:padding="@dimen/dynamic_grid_edge_margin">
-
-    <com.android.launcher3.search.SearchResultIcon
-        android:layout_width="wrap_content"
-        android:id="@+id/icon"
-        launcher:iconDisplay="hero_app"
-        android:layout_height="wrap_content" />
-
-    <LinearLayout
-        android:layout_width="0dp"
-        android:layout_weight="1"
-        android:layout_height="wrap_content"
-        android:padding="@dimen/dynamic_grid_edge_margin"
-        android:orientation="vertical"
-        android:layout_gravity="center_vertical">
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:id="@id/title"
-            android:layout_height="wrap_content"
-            android:gravity="start|center_vertical"
-            android:maxLines="1"
-            android:textAlignment="viewStart"
-            android:textColor="?android:attr/textColorPrimary"
-            android:textSize="@dimen/search_hero_title_size" />
-
-        <TextView
-            android:layout_width="wrap_content"
-            android:id="@+id/subtitle"
-            android:maxLines="1"
-            android:textColor="?android:attr/textColorTertiary"
-            android:textSize="@dimen/search_hero_subtitle_size"
-            android:layout_height="wrap_content" />
-    </LinearLayout>
-
-    <com.android.launcher3.search.SearchResultIcon
-        android:id="@+id/shortcut_0"
-        style="@style/BaseIcon"
-        android:layout_width="@dimen/deep_shortcut_icon_size"
-        android:layout_height="match_parent"
-        android:gravity="start|center_vertical"
-        launcher:iconDisplay="shortcut_popup"
-        android:textSize="@dimen/search_hero_subtitle_size"
-        launcher:iconSizeOverride="@dimen/deep_shortcut_icon_size"
-        launcher:layoutHorizontal="false" />
-
-    <com.android.launcher3.search.SearchResultIcon
-        android:id="@+id/shortcut_1"
-        style="@style/BaseIcon"
-        android:layout_width="@dimen/deep_shortcut_icon_size"
-        android:layout_height="match_parent"
-        launcher:iconDisplay="shortcut_popup"
-        android:textSize="@dimen/search_hero_inline_button_size"
-        launcher:iconSizeOverride="@dimen/deep_shortcut_icon_size"
-        launcher:layoutHorizontal="false" />
-</com.android.launcher3.search.SearchResultIconRow>
\ No newline at end of file
diff --git a/quickstep/res/layout/search_result_people_item.xml b/quickstep/res/layout/search_result_people_item.xml
deleted file mode 100644
index 964300d..0000000
--- a/quickstep/res/layout/search_result_people_item.xml
+++ /dev/null
@@ -1,63 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2020 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.launcher3.search.SearchResultPeopleView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:gravity="center_vertical"
-    android:layout_height="wrap_content"
-    android:padding="8dp"
-    android:orientation="horizontal">
-
-    <View
-        android:id="@+id/icon"
-        android:layout_marginRight="8dp"
-        android:layout_width="@dimen/deep_shortcut_icon_size"
-        android:layout_height="@dimen/deep_shortcut_icon_size" />
-
-    <TextView
-        android:layout_width="0dp"
-        android:textColor="?android:attr/textColorPrimary"
-        android:id="@+id/title"
-        android:textSize="@dimen/search_hero_title_size"
-        android:layout_height="wrap_content"
-        android:layout_weight="1" />
-
-    <ImageButton
-        android:id="@+id/provider_0"
-        android:scaleType="fitCenter"
-        android:adjustViewBounds="true"
-        android:layout_margin="5dp"
-        android:background="?android:attr/selectableItemBackground"
-        android:layout_width="@dimen/deep_shortcut_icon_size"
-        android:layout_height="@dimen/deep_shortcut_icon_size" />
-
-    <ImageButton
-        android:id="@+id/provider_1"
-        android:layout_margin="5dp"
-        android:scaleType="fitCenter"
-        android:adjustViewBounds="true"
-        android:background="?android:attr/selectableItemBackground"
-        android:layout_width="@dimen/deep_shortcut_icon_size"
-        android:layout_height="@dimen/deep_shortcut_icon_size" />
-
-    <ImageButton
-        android:id="@+id/provider_2"
-        android:layout_margin="5dp"
-        android:scaleType="fitCenter"
-        android:adjustViewBounds="true"
-        android:background="?android:attr/selectableItemBackground"
-        android:layout_width="@dimen/deep_shortcut_icon_size"
-        android:layout_height="@dimen/deep_shortcut_icon_size" />
-
-</com.android.launcher3.search.SearchResultPeopleView>
\ No newline at end of file
diff --git a/quickstep/res/layout/search_result_slice.xml b/quickstep/res/layout/search_result_slice.xml
deleted file mode 100644
index 4f884ff..0000000
--- a/quickstep/res/layout/search_result_slice.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2020 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.launcher3.search.SearchResultIconSlice xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    android:padding="@dimen/dynamic_grid_edge_margin"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content">
-
-    <FrameLayout
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content">
-
-        <com.android.launcher3.search.SearchResultIcon
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:id="@+id/icon"
-            launcher:iconDisplay="hero_app" />
-    </FrameLayout>
-
-    <androidx.slice.widget.SliceView
-        android:id="@+id/slice"
-        android:layout_height="wrap_content"
-        android:layout_weight="1"
-        android:layout_marginStart="@dimen/dynamic_grid_cell_padding_x"
-        android:layout_width="0dp" />
-
-</com.android.launcher3.search.SearchResultIconSlice>
-
diff --git a/quickstep/res/layout/search_result_widget_live.xml b/quickstep/res/layout/search_result_widget_live.xml
deleted file mode 100644
index f2ac6cd..0000000
--- a/quickstep/res/layout/search_result_widget_live.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<com.android.launcher3.search.SearchResultWidget
-    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_height="wrap_content"
-    android:orientation="vertical"
-    android:paddingVertical="@dimen/widget_section_vertical_padding"
-    android:layout_marginBottom="@dimen/widget_section_vertical_padding"
-    android:gravity="center"
-    >
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:orientation="horizontal"
-        android:layout_height="wrap_content">
-
-        <com.android.launcher3.BubbleTextView
-            android:id="@+id/widget_provider"
-            android:layout_width="wrap_content"
-            android:layout_height="@dimen/widget_section_height"
-            android:drawablePadding="@dimen/widget_section_horizontal_padding"
-            android:focusable="true"
-            android:gravity="start|center_vertical"
-            android:paddingHorizontal="@dimen/widget_section_horizontal_padding"
-            android:paddingVertical="@dimen/widget_section_horizontal_padding"
-            android:singleLine="true"
-            android:textColor="?android:attr/textColorPrimary"
-            android:textSize="16sp"
-            android:textAlignment="viewStart"
-            launcher:iconDisplay="widget_section"
-            launcher:layoutHorizontal="true"
-            launcher:iconSizeOverride="@dimen/widget_section_icon_size" />
-
-        <TextView
-            android:id="@+id/widget_label"
-            android:layout_width="wrap_content"
-            android:layout_height="@dimen/widget_section_height"
-            android:textSize="16sp" />
-
-    </LinearLayout>
-
-</com.android.launcher3.search.SearchResultWidget>
\ No newline at end of file
diff --git a/quickstep/res/layout/search_result_widget_preview.xml b/quickstep/res/layout/search_result_widget_preview.xml
deleted file mode 100644
index 7af24a1..0000000
--- a/quickstep/res/layout/search_result_widget_preview.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<com.android.launcher3.search.SearchResultWidgetPreview xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:padding="@dimen/dynamic_grid_cell_padding_x"
-    android:layout_height="wrap_content">
-    <include layout="@layout/widget_cell" android:id="@+id/widget_cell"/>
-<!--    <include layout="@layout/widget_cell_content" />-->
-</com.android.launcher3.search.SearchResultWidgetPreview>
\ No newline at end of file
diff --git a/quickstep/res/layout/search_section_title.xml b/quickstep/res/layout/search_section_title.xml
deleted file mode 100644
index 5842e57..0000000
--- a/quickstep/res/layout/search_section_title.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.launcher3.search.SearchSectionHeaderView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/section_title"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    style="@style/TextHeadline"
-    android:paddingStart="4dp"
-    android:paddingBottom="2dp"
-    android:paddingTop="12dp"
-    android:textColor="?android:attr/textColorPrimary"
-    android:textSize="18sp" />
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index f9bb2f2..0f9a6aa 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -17,8 +17,8 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:clipChildren="false"
     android:defaultFocusHighlightEnabled="false"
-    android:elevation="4dp"
     android:focusable="true">
 
     <com.android.quickstep.views.TaskThumbnailView
diff --git a/quickstep/res/layout/task_menu.xml b/quickstep/res/layout/task_menu.xml
index 098b34f..3916ff9 100644
--- a/quickstep/res/layout/task_menu.xml
+++ b/quickstep/res/layout/task_menu.xml
@@ -24,20 +24,12 @@
     android:orientation="vertical"
     android:visibility="invisible">
 
-    <com.android.quickstep.views.IconView
-      android:id="@+id/task_icon"
-      android:layout_width="@dimen/task_thumbnail_icon_size"
-      android:layout_height="@dimen/task_thumbnail_icon_size"
-      android:layout_gravity="top|center_horizontal"
-      android:layout_marginBottom="@dimen/deep_shortcut_drawable_padding"
-      android:focusable="false"
-      android:importantForAccessibility="no" />
-
     <TextView
         android:id="@+id/task_name"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:gravity="center_horizontal"
+        android:layout_marginTop="16dp"
         android:layout_marginBottom="16dp"
         android:textSize="12sp"/>
 
diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml
index 5f1046d..b124b33 100644
--- a/quickstep/res/layout/taskbar.xml
+++ b/quickstep/res/layout/taskbar.xml
@@ -24,6 +24,8 @@
         android:id="@+id/taskbar_view"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:background="@color/taskbar_background"/>
+        android:background="@color/taskbar_background"
+        android:gravity="center"
+        android:animateLayoutChanges="true"/>
 
 </com.android.launcher3.taskbar.TaskbarContainerView>
\ No newline at end of file
diff --git a/quickstep/res/layout/search_result_thumbnail.xml b/quickstep/res/layout/taskbar_app_icon.xml
similarity index 71%
rename from quickstep/res/layout/search_result_thumbnail.xml
rename to quickstep/res/layout/taskbar_app_icon.xml
index 5062b76..6fefdb6 100644
--- a/quickstep/res/layout/search_result_thumbnail.xml
+++ b/quickstep/res/layout/taskbar_app_icon.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
+<!-- Copyright (C) 2021 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.
@@ -13,7 +13,5 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.search.SearchResultThumbnailView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="125dp"
-    android:layout_height="125dp"/>
\ No newline at end of file
+
+<com.android.launcher3.views.DoubleShadowBubbleTextView style="@style/BaseIcon.Workspace.Taskbar" />
diff --git a/quickstep/res/layout/search_result_thumbnail.xml b/quickstep/res/layout/taskbar_divider.xml
similarity index 65%
copy from quickstep/res/layout/search_result_thumbnail.xml
copy to quickstep/res/layout/taskbar_divider.xml
index 5062b76..6e1aa1e 100644
--- a/quickstep/res/layout/search_result_thumbnail.xml
+++ b/quickstep/res/layout/taskbar_divider.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
+<!-- Copyright (C) 2021 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.
@@ -13,7 +13,11 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.search.SearchResultThumbnailView
+
+<View
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="125dp"
-    android:layout_height="125dp"/>
\ No newline at end of file
+    android:layout_width="@dimen/taskbar_divider_thickness"
+    android:layout_height="@dimen/taskbar_divider_height"
+    android:layout_marginStart="@dimen/taskbar_icon_spacing"
+    android:layout_marginEnd="@dimen/taskbar_icon_spacing"
+    android:background="@color/taskbar_divider" />
\ No newline at end of file
diff --git a/quickstep/res/layout/search_result_thumbnail.xml b/quickstep/res/layout/taskbar_predicted_app_icon.xml
similarity index 71%
copy from quickstep/res/layout/search_result_thumbnail.xml
copy to quickstep/res/layout/taskbar_predicted_app_icon.xml
index 5062b76..211ebc8 100644
--- a/quickstep/res/layout/search_result_thumbnail.xml
+++ b/quickstep/res/layout/taskbar_predicted_app_icon.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
+<!-- Copyright (C) 2021 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.
@@ -13,7 +13,5 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.search.SearchResultThumbnailView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="125dp"
-    android:layout_height="125dp"/>
\ No newline at end of file
+
+<com.android.launcher3.uioverrides.PredictedAppIcon style="@style/BaseIcon.Workspace.Taskbar" />
diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
index 3bc8ddc..54730f1 100644
--- a/quickstep/res/values/colors.xml
+++ b/quickstep/res/values/colors.xml
@@ -27,4 +27,5 @@
 
     <!-- Taskbar -->
     <color name="taskbar_background">#101010</color>
+    <color name="taskbar_divider">#C0C0C0</color>
 </resources>
\ No newline at end of file
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index 9ec303a..be66104 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -14,8 +14,6 @@
      limitations under the License.
 -->
 <resources>
-    <string name="task_overlay_factory_class" translatable="false"/>
-
     <string name="overscroll_plugin_factory_class" translatable="false" />
 
     <!-- Activities which block home gesture -->
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 4272f50..9773366 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -19,6 +19,7 @@
     <dimen name="task_thumbnail_top_margin">24dp</dimen>
     <dimen name="task_thumbnail_half_top_margin">12dp</dimen>
     <dimen name="task_thumbnail_icon_size">48dp</dimen>
+    <dimen name="task_icon_top_margin">-16dp</dimen>
     <!-- For screens without rounded corners -->
     <dimen name="task_corner_radius_small">2dp</dimen>
 
@@ -28,7 +29,8 @@
     <dimen name="overview_actions_bottom_margin_three_button">8dp</dimen>
     <dimen name="overview_actions_horizontal_margin">16dp</dimen>
 
-    <dimen name="recents_page_spacing">10dp</dimen>
+    <dimen name="recents_row_spacing">48dp</dimen>
+    <dimen name="recents_page_spacing">16dp</dimen>
     <dimen name="recents_clear_all_deadzone_vertical_margin">70dp</dimen>
 
     <!-- The speed in dp/s at which the user needs to be scrolling in recents such that we start
@@ -57,7 +59,7 @@
     <dimen name="task_card_menu_option_vertical_padding">8dp</dimen>
     <dimen name="task_card_menu_shadow_height">3dp</dimen>
     <dimen name="task_card_menu_horizontal_padding">0dp</dimen>
-    <dimen name="portrait_task_card_horz_space_big_overview">96dp</dimen>
+    <dimen name="portrait_task_card_horz_space_big_overview">132dp</dimen>
     <dimen name="portrait_modal_task_card_horz_space">60dp</dimen>
     <dimen name="landscape_task_card_horz_space">200dp</dimen>
     <dimen name="multi_window_task_card_horz_space">100dp</dimen>
@@ -121,5 +123,13 @@
     <dimen name="accessibility_gesture_min_swipe_distance">80dp</dimen>
 
     <!-- Taskbar -->
-    <dimen name="taskbar_size">48dp</dimen>
+    <dimen name="taskbar_size">60dp</dimen>
+    <dimen name="taskbar_icon_size">44dp</dimen>
+    <dimen name="taskbar_icon_touch_size">48dp</dimen>
+    <dimen name="taskbar_icon_drag_icon_size">54dp</dimen>
+    <!-- Note that this applies to both sides of all icons, so visible space is double this. -->
+    <dimen name="taskbar_icon_spacing">8dp</dimen>
+    <dimen name="taskbar_divider_thickness">1dp</dimen>
+    <dimen name="taskbar_divider_height">32dp</dimen>
+    <dimen name="taskbar_folder_margin">16dp</dimen>
 </resources>
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index 8d054b4..df089f6 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -85,4 +85,9 @@
         <item name="android:drawablePadding">8dp</item>
         <item name="android:textAllCaps">false</item>
     </style>
+
+    <!-- Icon displayed on the taskbar -->
+    <style name="BaseIcon.Workspace.Taskbar" >
+        <item name="iconDisplay">taskbar</item>
+    </style>
 </resources>
\ No newline at end of file
diff --git a/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
new file mode 100644
index 0000000..70a143e
--- /dev/null
+++ b/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2021 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.CONTAINER_WIDGETS_PREDICTION;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetId;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Process;
+import android.os.UserHandle;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.ViewOnDrawExecutor;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowAppWidgetManager;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsPredicationUpdateTaskTest {
+
+    private AppWidgetProviderInfo mApp1Provider1 = new AppWidgetProviderInfo();
+    private AppWidgetProviderInfo mApp1Provider2 = new AppWidgetProviderInfo();
+    private AppWidgetProviderInfo mApp2Provider1 = new AppWidgetProviderInfo();
+    private AppWidgetProviderInfo mApp4Provider1 = new AppWidgetProviderInfo();
+    private AppWidgetProviderInfo mApp4Provider2 = new AppWidgetProviderInfo();
+    private AppWidgetProviderInfo mApp5Provider1 = new AppWidgetProviderInfo();
+
+    private FakeBgDataModelCallback mCallback = new FakeBgDataModelCallback();
+    private Context mContext;
+    private LauncherModelHelper mModelHelper;
+    private UserHandle mUserHandle;
+    private InvariantDeviceProfile mTestProfile;
+
+    @Mock
+    private IconCache mIconCache;
+
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        doAnswer(invocation -> {
+            ComponentWithLabel componentWithLabel = invocation.getArgument(0);
+            return componentWithLabel.getComponent().getShortClassName();
+        }).when(mIconCache).getTitleNoCache(any());
+
+        mContext = RuntimeEnvironment.application;
+        mModelHelper = new LauncherModelHelper();
+        mUserHandle = Process.myUserHandle();
+        mTestProfile = new InvariantDeviceProfile();
+        // 2 widgets, app4/provider1 & app5/provider1, have already been added to the workspace.
+        mModelHelper.initializeData("/widgets_predication_update_task_data.txt");
+
+        ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+        mApp1Provider1.provider = ComponentName.createRelative("app1", "provider1");
+        ReflectionHelpers.setField(mApp1Provider1, "providerInfo",
+                packageManager.addReceiverIfNotPresent(mApp1Provider1.provider));
+        mApp1Provider2.provider = ComponentName.createRelative("app1", "provider2");
+        ReflectionHelpers.setField(mApp1Provider2, "providerInfo",
+                packageManager.addReceiverIfNotPresent(mApp1Provider2.provider));
+        mApp2Provider1.provider = ComponentName.createRelative("app2", "provider1");
+        ReflectionHelpers.setField(mApp2Provider1, "providerInfo",
+                packageManager.addReceiverIfNotPresent(mApp2Provider1.provider));
+        mApp4Provider1.provider = ComponentName.createRelative("app4", "provider1");
+        ReflectionHelpers.setField(mApp4Provider1, "providerInfo",
+                packageManager.addReceiverIfNotPresent(mApp4Provider1.provider));
+        mApp4Provider2.provider = ComponentName.createRelative("app4", ".provider2");
+        ReflectionHelpers.setField(mApp4Provider2, "providerInfo",
+                packageManager.addReceiverIfNotPresent(mApp4Provider2.provider));
+        mApp5Provider1.provider = ComponentName.createRelative("app5", "provider1");
+        ReflectionHelpers.setField(mApp5Provider1, "providerInfo",
+                packageManager.addReceiverIfNotPresent(mApp5Provider1.provider));
+
+        ShadowAppWidgetManager shadowAppWidgetManager =
+                shadowOf(mContext.getSystemService(AppWidgetManager.class));
+        shadowAppWidgetManager.addInstalledProvider(mApp1Provider1);
+        shadowAppWidgetManager.addInstalledProvider(mApp1Provider2);
+        shadowAppWidgetManager.addInstalledProvider(mApp2Provider1);
+        shadowAppWidgetManager.addInstalledProvider(mApp4Provider1);
+        shadowAppWidgetManager.addInstalledProvider(mApp4Provider2);
+        shadowAppWidgetManager.addInstalledProvider(mApp5Provider1);
+
+        mModelHelper.getModel().addCallbacks(mCallback);
+
+        MODEL_EXECUTOR.post(() -> mModelHelper.getBgDataModel().widgetsModel.update(
+                LauncherAppState.getInstance(mContext), /* packageUser= */ null));
+        waitUntilIdle();
+    }
+
+
+    @Test
+    public void widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder()
+            throws Exception {
+        // WHEN newPredicationTask is executed with app predication of 5 apps.
+        AppTarget app1 = new AppTarget(new AppTargetId("app1"), "app1", "className",
+                mUserHandle);
+        AppTarget app2 = new AppTarget(new AppTargetId("app2"), "app2", "className",
+                mUserHandle);
+        AppTarget app3 = new AppTarget(new AppTargetId("app3"), "app3", "className",
+                mUserHandle);
+        AppTarget app4 = new AppTarget(new AppTargetId("app4"), "app4", "className",
+                mUserHandle);
+        AppTarget app5 = new AppTarget(new AppTargetId("app5"), "app5", "className",
+                mUserHandle);
+        mModelHelper.executeTaskForTest(
+                newWidgetsPredicationTask(List.of(app5, app3, app2, app4, app1)))
+                .forEach(Runnable::run);
+
+        // THEN only 3 widgets are returned because
+        // 1. app5/provider1 & app4/provider1 have already been added to workspace. They are
+        //    excluded from the result.
+        // 2. app3 doesn't have a widget.
+        // 3. only 1 widget is picked from app1 because we only want to promote one widget per app.
+        List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
+                .stream()
+                .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
+                .collect(Collectors.toList());
+        assertThat(recommendedWidgets).hasSize(3);
+        assertWidgetInfo(recommendedWidgets.get(0).info, mApp2Provider1);
+        assertWidgetInfo(recommendedWidgets.get(1).info, mApp4Provider2);
+        assertWidgetInfo(recommendedWidgets.get(2).info, mApp1Provider1);
+    }
+
+    private void assertWidgetInfo(
+            LauncherAppWidgetProviderInfo actual, AppWidgetProviderInfo expected) {
+        assertThat(actual.provider).isEqualTo(expected.provider);
+        assertThat(actual.getUser()).isEqualTo(expected.getProfile());
+    }
+
+    private void waitUntilIdle() {
+        shadowOf(MODEL_EXECUTOR.getLooper()).idle();
+        shadowOf(MAIN_EXECUTOR.getLooper()).idle();
+    }
+
+    private WidgetsPredictionUpdateTask newWidgetsPredicationTask(List<AppTarget> appTargets) {
+        return new WidgetsPredictionUpdateTask(
+                new PredictorState(CONTAINER_WIDGETS_PREDICTION, "test_widgets_prediction"),
+                appTargets);
+    }
+
+    private final class FakeBgDataModelCallback implements BgDataModel.Callbacks {
+
+        private FixedContainerItems mRecommendedWidgets = null;
+
+        @Override
+        public void bindExtraContainerItems(FixedContainerItems item) {
+            mRecommendedWidgets = item;
+        }
+
+        @Override
+        public int getPageToBindSynchronously() {
+            return 0;
+        }
+
+        @Override
+        public void clearPendingBinds() { }
+
+        @Override
+        public void startBinding() { }
+
+        @Override
+        public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }
+
+        @Override
+        public void bindScreens(IntArray orderedScreenIds) { }
+
+        @Override
+        public void finishFirstPageBind(ViewOnDrawExecutor executor) { }
+
+        @Override
+        public void finishBindingItems(int pageBoundFirst) { }
+
+        @Override
+        public void preAddApps() { }
+
+        @Override
+        public void bindAppsAdded(IntArray newScreens, ArrayList<ItemInfo> addNotAnimated,
+                ArrayList<ItemInfo> addAnimated) { }
+
+        @Override
+        public void bindIncrementalDownloadProgressUpdated(AppInfo app) { }
+
+        @Override
+        public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) { }
+
+        @Override
+        public void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) { }
+
+        @Override
+        public void bindRestoreItemsChange(HashSet<ItemInfo> updates) { }
+
+        @Override
+        public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { }
+
+        @Override
+        public void bindAllWidgets(List<WidgetsListBaseEntry> widgets) { }
+
+        @Override
+        public void onPageBoundSynchronously(int page) { }
+
+        @Override
+        public void executeOnNextDraw(ViewOnDrawExecutor executor) { }
+
+        @Override
+        public void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap) { }
+
+        @Override
+        public void bindAllApplications(AppInfo[] apps, int flags) { }
+    }
+}
diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
index 5cb55ec..eca27b5 100644
--- a/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
+++ b/quickstep/robolectric_tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
@@ -43,8 +43,18 @@
 
 @RunWith(RobolectricTestRunner.class)
 public class OrientationTouchTransformerTest {
-    private static final int SIZE_WIDTH = 1080;
-    private static final int SIZE_HEIGHT = 2280;
+    static class ScreenSize {
+        int mHeight;
+        int mWidth;
+
+        ScreenSize(int height, int width) {
+            mHeight = height;
+            mWidth = width;
+        }
+    }
+
+    private static final ScreenSize NORMAL_SCREEN_SIZE = new ScreenSize(2280, 1080);
+    private static final ScreenSize LARGE_SCREEN_SIZE = new ScreenSize(3280, 1080);
     private static final float DENSITY_DISPLAY_METRICS = 3.0f;
 
     private OrientationTouchTransformer mTouchTransformer;
@@ -63,14 +73,16 @@
         DisplayMetrics mockDisplayMetrics = new DisplayMetrics();
         mockDisplayMetrics.density = DENSITY_DISPLAY_METRICS;
         when(mResources.getDisplayMetrics()).thenReturn(mockDisplayMetrics);
-        mInfo = createDisplayInfo(Surface.ROTATION_0);
+        mInfo = createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_0);
         mTouchTransformer = new OrientationTouchTransformer(mResources, NO_BUTTON, () -> 0);
     }
 
     @Test
     public void disabledMultipleRegions_shouldOverrideFirstRegion() {
-        float portraitRegionY = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
-        float landscapeRegionY = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
+        float portraitRegionY =
+                generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
+        float landscapeRegionY =
+                generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
 
         mTouchTransformer.createOrAddTouchRegion(mInfo);
         tapAndAssertTrue(100, portraitRegionY,
@@ -83,7 +95,8 @@
                 event -> mTouchTransformer.touchInAssistantRegion(event));
 
         // Override region
-        mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
+        mTouchTransformer
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
         tapAndAssertFalse(100, portraitRegionY,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
         tapAndAssertTrue(100, landscapeRegionY,
@@ -107,10 +120,13 @@
 
     @Test
     public void enableMultipleRegions_shouldOverrideFirstRegion() {
-        float portraitRegionY = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
-        float landscapeRegionY = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
+        float portraitRegionY =
+                generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
+        float landscapeRegionY =
+                generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
 
-        mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
+        mTouchTransformer
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
         tapAndAssertFalse(100, portraitRegionY,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
         tapAndAssertTrue(100, landscapeRegionY,
@@ -136,11 +152,14 @@
 
     @Test
     public void enableMultipleRegions_assistantTriggersInMostRecent() {
-        float portraitRegionY = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
-        float landscapeRegionY = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
+        float portraitRegionY =
+                generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
+        float landscapeRegionY =
+                generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
 
         mTouchTransformer.enableMultipleRegions(true, mInfo);
-        mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
+        mTouchTransformer
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
         mTouchTransformer.createOrAddTouchRegion(mInfo);
         tapAndAssertTrue(0, portraitRegionY,
                 event -> mTouchTransformer.touchInAssistantRegion(event));
@@ -150,12 +169,15 @@
 
     @Test
     public void enableMultipleRegions_assistantTriggersInCurrentOrientationAfterDisable() {
-        float portraitRegionY = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
-        float landscapeRegionY = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
+        float portraitRegionY =
+                generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
+        float landscapeRegionY =
+                generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
 
         mTouchTransformer.enableMultipleRegions(true, mInfo);
         mTouchTransformer.createOrAddTouchRegion(mInfo);
-        mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
+        mTouchTransformer
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
         mTouchTransformer.enableMultipleRegions(false, mInfo);
         tapAndAssertTrue(0, portraitRegionY,
                 event -> mTouchTransformer.touchInAssistantRegion(event));
@@ -164,6 +186,26 @@
     }
 
     @Test
+    public void assistantTriggersInCurrentScreenAfterScreenSizeChange() {
+        float smallerScreenPortraitRegionY =
+                generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
+        float largerScreenPortraitRegionY =
+                generateTouchRegionHeight(LARGE_SCREEN_SIZE, Surface.ROTATION_0) + 1;
+
+        mTouchTransformer.enableMultipleRegions(false,
+                createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_0));
+        tapAndAssertTrue(0, smallerScreenPortraitRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+
+        mTouchTransformer
+            .enableMultipleRegions(false, createDisplayInfo(LARGE_SCREEN_SIZE, Surface.ROTATION_0));
+        tapAndAssertTrue(0, largerScreenPortraitRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+        tapAndAssertFalse(0, smallerScreenPortraitRegionY,
+                event -> mTouchTransformer.touchInAssistantRegion(event));
+    }
+
+    @Test
     public void applyTransform_taskNotFrozen_notInRegion() {
         mTouchTransformer.createOrAddTouchRegion(mInfo);
         tapAndAssertFalse(100, 100,
@@ -182,7 +224,7 @@
     public void applyTransform_taskFrozen_noRotate_inRegion() {
         mTouchTransformer.createOrAddTouchRegion(mInfo);
         mTouchTransformer.enableMultipleRegions(true, mInfo);
-        float y = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
+        float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
         tapAndAssertTrue(100, y,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
     }
@@ -190,15 +232,16 @@
     @Test
     public void applyTransform_taskNotFrozen_noRotate_inDefaultRegion() {
         mTouchTransformer.createOrAddTouchRegion(mInfo);
-        float y = generateTouchRegionHeight(Surface.ROTATION_0) + 1;
+        float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_0) + 1;
         tapAndAssertTrue(100, y,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
     }
 
     @Test
     public void applyTransform_taskNotFrozen_90Rotate_inRegion() {
-        mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
-        float y = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
+        mTouchTransformer
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
+        float y = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
         tapAndAssertTrue(100, y,
                 event -> mTouchTransformer.touchInValidSwipeRegions(event.getX(), event.getY()));
     }
@@ -210,9 +253,10 @@
     public void applyTransform_taskNotFrozen_90Rotate_inTwoRegions() {
         mTouchTransformer.createOrAddTouchRegion(mInfo);
         mTouchTransformer.enableMultipleRegions(true, mInfo);
-        mTouchTransformer.createOrAddTouchRegion(createDisplayInfo(Surface.ROTATION_90));
+        mTouchTransformer
+            .createOrAddTouchRegion(createDisplayInfo(NORMAL_SCREEN_SIZE, Surface.ROTATION_90));
         // Landscape point
-        float y1 = generateTouchRegionHeight(Surface.ROTATION_90) + 1;
+        float y1 = generateTouchRegionHeight(NORMAL_SCREEN_SIZE, Surface.ROTATION_90) + 1;
         MotionEvent inRegion1_down = generateMotionEvent(MotionEvent.ACTION_DOWN, 10, y1);
         MotionEvent inRegion1_up = generateMotionEvent(MotionEvent.ACTION_UP, 10, y1);
         // Portrait point in landscape orientation axis
@@ -231,18 +275,18 @@
         assertTrue(mTouchTransformer.touchInValidSwipeRegions(inRegion2.getX(), inRegion2.getY()));
     }
 
-    private DisplayController.Info createDisplayInfo(int rotation) {
-        Point p = new Point(SIZE_WIDTH, SIZE_HEIGHT);
+    private DisplayController.Info createDisplayInfo(ScreenSize screenSize, int rotation) {
+        Point p = new Point(screenSize.mWidth, screenSize.mHeight);
         if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
-            p = new Point(SIZE_HEIGHT, SIZE_WIDTH);
+            p = new Point(screenSize.mHeight, screenSize.mWidth);
         }
         return new DisplayController.Info(0, rotation, 0, p, p, p, null);
     }
 
-    private float generateTouchRegionHeight(int rotation) {
-        float height = SIZE_HEIGHT;
+    private float generateTouchRegionHeight(ScreenSize screenSize, int rotation) {
+        float height = screenSize.mHeight;
         if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
-            height = SIZE_WIDTH;
+            height = screenSize.mWidth;
         }
         return height - ResourceUtils.DEFAULT_NAVBAR_VALUE * DENSITY_DISPLAY_METRICS;
     }
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 2518f42..161c98e 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -30,12 +30,12 @@
 import android.content.IntentSender;
 import android.os.Bundle;
 import android.os.CancellationSignal;
-import android.view.LayoutInflater;
 import android.view.View;
 
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.model.WellbeingModel;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.proxy.ProxyActivityStarter;
@@ -43,8 +43,9 @@
 import com.android.launcher3.statehandlers.BackButtonAlphaHandler;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
-import com.android.launcher3.taskbar.TaskbarContainerView;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarController;
+import com.android.launcher3.taskbar.TaskbarStateHandler;
 import com.android.launcher3.uioverrides.RecentsViewStateController;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.UiThreadHelper;
@@ -82,6 +83,9 @@
     private OverviewActionsView mActionsView;
 
     private @Nullable TaskbarController mTaskbarController;
+    private final TaskbarStateHandler mTaskbarStateHandler = new TaskbarStateHandler(this);
+    // Will be updated when dragging from taskbar.
+    private DragOptions mWorkspaceDragOptions = new DragOptions();
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -205,6 +209,7 @@
         mActionsView.updateVerticalMargin(SysUINavigationMode.getMode(this));
 
         addTaskbarIfNecessary();
+        addOnDeviceProfileChangeListener(newDp -> addTaskbarIfNecessary());
     }
 
     @Override
@@ -220,10 +225,10 @@
             mTaskbarController.cleanup();
             mTaskbarController = null;
         }
-        if (FeatureFlags.ENABLE_TASKBAR.get() && mDeviceProfile.isTablet) {
-            TaskbarContainerView taskbarContainer = (TaskbarContainerView) LayoutInflater.from(this)
-                    .inflate(R.layout.taskbar, null, false);
-            mTaskbarController = new TaskbarController(this, taskbarContainer);
+        if (mDeviceProfile.isTaskbarPresent) {
+            TaskbarActivityContext taskbarActivityContext = new TaskbarActivityContext(this);
+            mTaskbarController = new TaskbarController(this,
+                    taskbarActivityContext.getTaskbarContainerView());
             mTaskbarController.init();
         }
     }
@@ -245,13 +250,37 @@
                 getWorkspace(),
                 getDepthController(),
                 new RecentsViewStateController(this),
-                new BackButtonAlphaHandler(this)};
+                new BackButtonAlphaHandler(this),
+                getTaskbarStateHandler(),
+        };
     }
 
     public DepthController getDepthController() {
         return mDepthController;
     }
 
+    public @Nullable TaskbarController getTaskbarController() {
+        return mTaskbarController;
+    }
+
+    public TaskbarStateHandler getTaskbarStateHandler() {
+        return mTaskbarStateHandler;
+    }
+
+    @Override
+    public boolean isViewInTaskbar(View v) {
+        return mTaskbarController != null && mTaskbarController.isViewInTaskbar(v);
+    }
+
+    @Override
+    public DragOptions getDefaultWorkspaceDragOptions() {
+        return mWorkspaceDragOptions;
+    }
+
+    public void setWorkspaceDragOptions(DragOptions dragOptions) {
+        mWorkspaceDragOptions = dragOptions;
+    }
+
     @Override
     public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) {
         QuickstepAppTransitionManagerImpl appTransitionManager =
@@ -296,6 +325,12 @@
             mDepthController.setActivityStarted(isStarted());
         }
 
+        if ((changeBits & ACTIVITY_STATE_RESUMED) != 0) {
+            if (mTaskbarController != null) {
+                mTaskbarController.onLauncherResumedOrPaused(hasBeenResumed());
+            }
+        }
+
         super.onActivityFlagsChanged(changeBits);
     }
 
diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
index 034d51f..588d676 100644
--- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java
@@ -56,17 +56,22 @@
         return mHandler;
     }
 
-    // Called only in R+ platform
+    // Called only in S+ platform
     @BinderThread
-    public void onAnimationStart(RemoteAnimationTargetCompat[] appTargets,
-            RemoteAnimationTargetCompat[] wallpaperTargets, Runnable runnable) {
+    public void onAnimationStart(
+            int transit,
+            RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets,
+            RemoteAnimationTargetCompat[] nonAppTargets,
+            Runnable runnable) {
         Runnable r = () -> {
             finishExistingAnimation();
             mAnimationResult = new AnimationResult(() -> {
                 UI_HELPER_EXECUTOR.execute(runnable);
                 mAnimationResult = null;
             });
-            onCreateAnimation(appTargets, wallpaperTargets, mAnimationResult);
+            onCreateAnimation(transit, appTargets, wallpaperTargets, nonAppTargets,
+                    mAnimationResult);
         };
         if (mStartAtFrontOfQueue) {
             postAtFrontOfQueueAsynchronously(mHandler, r);
@@ -75,6 +80,14 @@
         }
     }
 
+    // Called only in R platform
+    @BinderThread
+    public void onAnimationStart(RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets, Runnable runnable) {
+        onAnimationStart(0 /* transit */, appTargets, wallpaperTargets,
+                new RemoteAnimationTargetCompat[0], runnable);
+    }
+
     // Called only in Q platform
     @BinderThread
     @Deprecated
@@ -88,8 +101,11 @@
      */
     @UiThread
     public abstract void onCreateAnimation(
+            int transit,
             RemoteAnimationTargetCompat[] appTargets,
-            RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result);
+            RemoteAnimationTargetCompat[] wallpaperTargets,
+            RemoteAnimationTargetCompat[] nonAppTargets,
+            AnimationResult result);
 
     @UiThread
     private void finishExistingAnimation() {
diff --git a/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java
new file mode 100644
index 0000000..96559cb
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3;
+
+import android.view.KeyEvent;
+import android.view.View;
+
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.uioverrides.PredictedAppIcon;
+import com.android.launcher3.uioverrides.QuickstepLauncher;
+
+import java.util.List;
+
+public class QuickstepAccessibilityDelegate extends LauncherAccessibilityDelegate {
+
+    public QuickstepAccessibilityDelegate(QuickstepLauncher launcher) {
+        super(launcher);
+        mActions.put(PIN_PREDICTION, new LauncherAction(
+                PIN_PREDICTION, R.string.pin_prediction, KeyEvent.KEYCODE_P));
+    }
+
+    @Override
+    protected void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out) {
+        if (host instanceof PredictedAppIcon && !((PredictedAppIcon) host).isPinned()) {
+            out.add(new LauncherAction(PIN_PREDICTION, R.string.pin_prediction,
+                    KeyEvent.KEYCODE_P));
+        }
+        super.getSupportedActions(host, item, out);
+    }
+
+    @Override
+    protected boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) {
+        QuickstepLauncher launcher = (QuickstepLauncher) mLauncher;
+        if (action == PIN_PREDICTION) {
+            if (launcher.getHotseatPredictionController() == null) {
+                return false;
+            }
+            launcher.getHotseatPredictionController().pinPrediction(item);
+            return true;
+        }
+        return super.performAction(host, item, action, fromKeyboard);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
index 470a442..c4b6961 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
@@ -139,7 +139,7 @@
     private static final int LAUNCHER_RESUME_START_DELAY = 100;
     private static final int CLOSING_TRANSITION_DURATION_MS = 250;
 
-    protected static final int CONTENT_ALPHA_DURATION = 217;
+    public static final int CONTENT_ALPHA_DURATION = 217;
     protected static final int CONTENT_TRANSLATION_DURATION = 350;
 
     // Progress = 0: All apps is fully pulled up, Progress = 1: All apps is fully pulled down.
@@ -202,9 +202,10 @@
     }
 
     @Override
-    public boolean supportsAdaptiveIconAnimation() {
+    public boolean supportsAdaptiveIconAnimation(View clickedView) {
         return hasControlRemoteAppTransitionPermission()
-                && FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM.get();
+                && FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM.get()
+                && !mLauncher.isViewInTaskbar(clickedView);
     }
 
     /**
@@ -865,8 +866,10 @@
         }
 
         @Override
-        public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
+        public void onCreateAnimation(int transit,
+                RemoteAnimationTargetCompat[] appTargets,
                 RemoteAnimationTargetCompat[] wallpaperTargets,
+                RemoteAnimationTargetCompat[] nonAppTargets,
                 LauncherAnimationRunner.AnimationResult result) {
             if (mLauncher.isDestroyed()) {
                 AnimatorSet anim = new AnimatorSet();
@@ -879,7 +882,8 @@
                 // If launcher is not resumed, wait until new async-frame after resume
                 mLauncher.addOnResumeCallback(() ->
                         postAsyncCallback(mHandler, () ->
-                                onCreateAnimation(appTargets, wallpaperTargets, result)));
+                                onCreateAnimation(transit, appTargets, wallpaperTargets,
+                                        nonAppTargets, result)));
                 return;
             }
 
@@ -963,8 +967,10 @@
         }
 
         @Override
-        public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
+        public void onCreateAnimation(int transit,
+                RemoteAnimationTargetCompat[] appTargets,
                 RemoteAnimationTargetCompat[] wallpaperTargets,
+                RemoteAnimationTargetCompat[] nonAppTargets,
                 LauncherAnimationRunner.AnimationResult result) {
             AnimatorSet anim = new AnimatorSet();
 
@@ -972,9 +978,12 @@
                     launcherIsATargetWithMode(appTargets, MODE_CLOSING);
 
             final boolean launchingFromRecents = isLaunchingFromRecents(mV, appTargets);
+            final boolean launchingFromTaskbar = mLauncher.isViewInTaskbar(mV);
             if (launchingFromRecents) {
                 composeRecentsLaunchAnimator(anim, mV, appTargets, wallpaperTargets,
                         launcherClosing);
+            } else if (launchingFromTaskbar) {
+                // TODO
             } else {
                 composeIconLaunchAnimator(anim, mV, appTargets, wallpaperTargets,
                         launcherClosing);
diff --git a/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java b/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java
index da2aee4..03cc28e 100644
--- a/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java
+++ b/quickstep/src/com/android/launcher3/WrappedAnimationRunnerImpl.java
@@ -26,7 +26,9 @@
  */
 public interface WrappedAnimationRunnerImpl {
     Handler getHandler();
-    void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
+    void onCreateAnimation(int transit,
+            RemoteAnimationTargetCompat[] appTargets,
             RemoteAnimationTargetCompat[] wallpaperTargets,
+            RemoteAnimationTargetCompat[] nonAppTargets,
             LauncherAnimationRunner.AnimationResult result);
 }
diff --git a/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java
index 1753b62..1e1631b 100644
--- a/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java
+++ b/quickstep/src/com/android/launcher3/WrappedLauncherAnimationRunner.java
@@ -46,11 +46,15 @@
     }
 
     @Override
-    public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
-            RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result) {
+    public void onCreateAnimation(int transit,
+            RemoteAnimationTargetCompat[] appTargets,
+            RemoteAnimationTargetCompat[] wallpaperTargets,
+            RemoteAnimationTargetCompat[] nonAppTargets,
+            AnimationResult result) {
         R animationRunnerImpl = mImpl.get();
         if (animationRunnerImpl != null) {
-            animationRunnerImpl.onCreateAnimation(appTargets, wallpaperTargets, result);
+            animationRunnerImpl.onCreateAnimation(transit, appTargets, wallpaperTargets,
+                    nonAppTargets, result);
         }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
index b891120..66b1a86 100644
--- a/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/AppsDividerView.java
@@ -42,7 +42,6 @@
 import com.android.launcher3.allapps.FloatingHeaderRow;
 import com.android.launcher3.allapps.FloatingHeaderView;
 import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.util.Themes;
 
@@ -183,11 +182,6 @@
     }
 
     private void updateViewVisibility() {
-        // hide divider since we have item decoration for prediction row
-        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
-            setVisibility(GONE);
-            return;
-        }
         setVisibility(mDividerType == DividerType.NONE
                 ? GONE
                 : (mIsScrolledOut ? INVISIBLE : VISIBLE));
diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
index f53a5ef..83234e3 100644
--- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -41,7 +41,6 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
-import com.android.launcher3.allapps.AllAppsSectionDecorator;
 import com.android.launcher3.allapps.FloatingHeaderRow;
 import com.android.launcher3.allapps.FloatingHeaderView;
 import com.android.launcher3.anim.AlphaUpdateListener;
@@ -106,9 +105,8 @@
 
     private boolean mPredictionsEnabled = false;
 
-    AllAppsSectionDecorator.SectionDecorationHandler mDecorationHandler;
-
-    @Nullable private List<ItemInfo> mPendingPredictedItems;
+    @Nullable
+    private List<ItemInfo> mPendingPredictedItems;
 
     public PredictionRowView(@NonNull Context context) {
         this(context, null);
@@ -128,11 +126,6 @@
         mIconFullTextAlpha = Color.alpha(mIconTextColor);
         mIconCurrentTextAlpha = mIconFullTextAlpha;
 
-        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
-            mDecorationHandler = new AllAppsSectionDecorator.SectionDecorationHandler(getContext(),
-                    false);
-        }
-
         updateVisibility();
     }
 
@@ -158,16 +151,6 @@
 
     @Override
     protected void dispatchDraw(Canvas canvas) {
-        if (mDecorationHandler != null) {
-            mDecorationHandler.reset();
-            int childrenCount = getChildCount();
-            for (int i = 0; i < childrenCount; i++) {
-                mDecorationHandler.extendBounds(getChildAt(i));
-            }
-            mDecorationHandler.onDraw(canvas);
-            mDecorationHandler.onFocusDraw(canvas, getFocusedChild());
-            mLauncher.getAppsView().getActiveRecyclerView().invalidateItemDecorations();
-        }
         mFocusHelper.draw(canvas);
         super.dispatchDraw(canvas);
     }
@@ -181,7 +164,7 @@
 
     @Override
     public boolean shouldDraw() {
-        return getVisibility() == VISIBLE;
+        return getVisibility() != GONE;
     }
 
     @Override
@@ -189,6 +172,11 @@
         return mPredictionsEnabled;
     }
 
+    @Override
+    public boolean isVisible() {
+        return getVisibility() == VISIBLE;
+    }
+
     /**
      * Returns the predicted apps.
      */
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
index c968de9..3a1a2f7 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduActivity.java
@@ -29,7 +29,6 @@
  */
 public class HotseatEduActivity extends Activity {
 
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -49,7 +48,7 @@
         @Override
         public boolean init(BaseActivity activity, boolean alreadyOnHome) {
             QuickstepLauncher launcher = (QuickstepLauncher) activity;
-            if (launcher != null && launcher.getHotseatPredictionController() != null) {
+            if (launcher != null) {
                 launcher.getHotseatPredictionController().showEdu();
             }
             return false;
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java
deleted file mode 100644
index 20e1edc..0000000
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatFileLog.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.hybridhotseat;
-
-import android.content.Context;
-import android.os.Handler;
-import android.util.Log;
-
-import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.util.Executors;
-import com.android.launcher3.util.MainThreadInitializedObject;
-
-import java.io.File;
-import java.io.FileWriter;
-import java.io.PrintWriter;
-import java.text.DateFormat;
-import java.util.Calendar;
-import java.util.Date;
-
-/**
- * Helper class to allow hot seat file logging
- */
-public class HotseatFileLog {
-
-    public static final int LOG_DAYS = 10;
-    private static final String FILE_NAME_PREFIX = "hotseat-log-";
-    private static final DateFormat DATE_FORMAT =
-            DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
-    public static final MainThreadInitializedObject<HotseatFileLog> INSTANCE =
-            new MainThreadInitializedObject<>(HotseatFileLog::new);
-
-
-    private final Handler mHandler = new Handler(
-            Executors.createAndStartNewLooper("hotseat-logger"));
-    private final File mLogsDir;
-    private PrintWriter mCurrentWriter;
-    private String mFileName;
-
-    private HotseatFileLog(Context context) {
-        mLogsDir = context.getFilesDir();
-    }
-
-    /**
-     * Prints log values to disk
-     */
-    public void log(String tag, String msg) {
-        String out = String.format("%s %s %s", DATE_FORMAT.format(new Date()), tag, msg);
-
-        mHandler.post(() -> {
-            synchronized (this) {
-                PrintWriter writer = getWriter();
-                if (writer != null) {
-                    writer.println(out);
-                }
-            }
-        });
-    }
-
-    private PrintWriter getWriter() {
-        Calendar cal = Calendar.getInstance();
-        String fName = FILE_NAME_PREFIX + (cal.get(Calendar.DAY_OF_YEAR) % 10);
-        if (fName.equals(mFileName)) return mCurrentWriter;
-
-        boolean append = false;
-        File logFile = new File(mLogsDir, fName);
-        if (logFile.exists()) {
-            Calendar modifiedTime = Calendar.getInstance();
-            modifiedTime.setTimeInMillis(logFile.lastModified());
-
-            // If the file was modified more that 36 hours ago, purge the file.
-            // We use instead of 24 to account for day-365 followed by day-1
-            modifiedTime.add(Calendar.HOUR, 36);
-            append = cal.before(modifiedTime);
-        }
-
-
-        if (mCurrentWriter != null) {
-            mCurrentWriter.close();
-        }
-        try {
-            mCurrentWriter = new PrintWriter(new FileWriter(logFile, append));
-            mFileName = fName;
-        } catch (Exception ex) {
-            Log.e("HotseatLogs", "Error writing logs to file", ex);
-            closeWriter();
-        }
-        return mCurrentWriter;
-    }
-
-
-    private synchronized void closeWriter() {
-        mFileName = null;
-        if (mCurrentWriter != null) {
-            mCurrentWriter.close();
-        }
-        mCurrentWriter = null;
-    }
-
-
-    /**
-     * Returns a list of all log files
-     */
-    public synchronized File[] getLogFiles() {
-        File[] files = new File[LOG_DAYS + FileLog.LOG_DAYS];
-        //include file log files here
-        System.arraycopy(FileLog.getLogFiles(), 0, files, 0, FileLog.LOG_DAYS);
-
-        closeWriter();
-        for (int i = 0; i < LOG_DAYS; i++) {
-            files[FileLog.LOG_DAYS + i] = new File(mLogsDir, FILE_NAME_PREFIX + i);
-        }
-        return files;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index b2de4c9..f297343 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -20,12 +20,12 @@
 import static com.android.launcher3.hybridhotseat.HotseatEduController.getSettingsIntent;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_PREDICTION_PINNED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_RANKED;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.content.ComponentName;
-import android.os.Process;
 import android.view.HapticFeedbackConstants;
 import android.view.View;
 import android.view.ViewGroup;
@@ -37,10 +37,8 @@
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragController;
@@ -71,18 +69,23 @@
  */
 public class HotseatPredictionController implements DragController.DragListener,
         SystemShortcut.Factory<QuickstepLauncher>, InvariantDeviceProfile.OnIDPChangeListener,
-        DragSource {
+        DragSource, ViewGroup.OnHierarchyChangeListener {
+
+    private static final int FLAG_UPDATE_PAUSED = 1 << 0;
+    private static final int FLAG_DRAG_IN_PROGRESS = 1 << 1;
+    private static final int FLAG_FILL_IN_PROGRESS = 1 << 2;
+    private static final int FLAG_REMOVING_PREDICTED_ICON = 1 << 3;
 
     private int mHotSeatItemsCount;
 
-    private Launcher mLauncher;
+    private QuickstepLauncher mLauncher;
     private final Hotseat mHotseat;
+    private final Runnable mUpdateFillIfNotLoading = this::updateFillIfNotLoading;
 
     private List<ItemInfo> mPredictedItems = Collections.emptyList();
 
     private AnimatorSet mIconRemoveAnimators;
-    private boolean mUIUpdatePaused = false;
-    private boolean mDragInProgress = false;
+    private int mPauseFlags = 0;
 
     private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>();
 
@@ -104,17 +107,43 @@
         WorkspaceItemInfo dragItem = new WorkspaceItemInfo((WorkspaceItemInfo) v.getTag());
         v.setVisibility(View.INVISIBLE);
         mLauncher.getWorkspace().beginDragShared(
-                v, null, this, dragItem, new DragPreviewProvider(v), new DragOptions());
+                v, null, this, dragItem, new DragPreviewProvider(v),
+                mLauncher.getDefaultWorkspaceDragOptions());
         return true;
     };
 
-    public HotseatPredictionController(Launcher launcher) {
+    public HotseatPredictionController(QuickstepLauncher launcher) {
         mLauncher = launcher;
         mHotseat = launcher.getHotseat();
         mHotSeatItemsCount = mLauncher.getDeviceProfile().inv.numHotseatIcons;
         mLauncher.getDragController().addDragListener(this);
 
         launcher.getDeviceProfile().inv.addOnChangeListener(this);
+        mHotseat.getShortcutsAndWidgets().setOnHierarchyChangeListener(this);
+    }
+
+    @Override
+    public void onChildViewAdded(View parent, View child) {
+        onHotseatHierarchyChanged();
+    }
+
+    @Override
+    public void onChildViewRemoved(View parent, View child) {
+        onHotseatHierarchyChanged();
+    }
+
+    private void onHotseatHierarchyChanged() {
+        if (mPauseFlags == 0 && !mLauncher.isWorkspaceLoading()) {
+            // Post update after a single frame to avoid layout within layout
+            MAIN_EXECUTOR.getHandler().removeCallbacks(mUpdateFillIfNotLoading);
+            MAIN_EXECUTOR.getHandler().post(mUpdateFillIfNotLoading);
+        }
+    }
+
+    private void updateFillIfNotLoading() {
+        if (mPauseFlags == 0 && !mLauncher.isWorkspaceLoading()) {
+            fillGapsWithPrediction(true);
+        }
     }
 
     /**
@@ -161,11 +190,11 @@
     }
 
     private void fillGapsWithPrediction() {
-        fillGapsWithPrediction(false, null);
+        fillGapsWithPrediction(false);
     }
 
-    private void fillGapsWithPrediction(boolean animate, Runnable callback) {
-        if (mUIUpdatePaused || mDragInProgress) {
+    private void fillGapsWithPrediction(boolean animate) {
+        if (mPauseFlags != 0) {
             return;
         }
 
@@ -176,12 +205,14 @@
             mIconRemoveAnimators.addListener(new AnimationSuccessListener() {
                 @Override
                 public void onAnimationSuccess(Animator animator) {
-                    fillGapsWithPrediction(animate, callback);
+                    fillGapsWithPrediction(animate);
                     mIconRemoveAnimators.removeListener(this);
                 }
             });
             return;
         }
+
+        mPauseFlags |= FLAG_FILL_IN_PROGRESS;
         for (int rank = 0; rank < mHotSeatItemsCount; rank++) {
             View child = mHotseat.getChildAt(
                     mHotseat.getCellXFromOrder(rank),
@@ -208,10 +239,12 @@
             }
             preparePredictionInfo(predictedItem, rank);
         }
-        bindItems(newItems, animate, callback);
+        bindItems(newItems, animate);
+
+        mPauseFlags &= ~FLAG_FILL_IN_PROGRESS;
     }
 
-    private void bindItems(List<WorkspaceItemInfo> itemsToAdd, boolean animate, Runnable callback) {
+    private void bindItems(List<WorkspaceItemInfo> itemsToAdd, boolean animate) {
         AnimatorSet animationSet = new AnimatorSet();
         for (WorkspaceItemInfo item : itemsToAdd) {
             PredictedAppIcon icon = PredictedAppIcon.createIcon(mHotseat, item);
@@ -222,15 +255,28 @@
             }
         }
         if (animate) {
-            if (callback != null) {
-                animationSet.addListener(AnimationSuccessListener.forRunnable(callback));
-            }
+            animationSet.addListener(AnimationSuccessListener
+                    .forRunnable(this::removeOutlineDrawings));
             animationSet.start();
         } else {
-            if (callback != null) callback.run();
+            removeOutlineDrawings();
+        }
+
+        if (mLauncher.getTaskbarController() != null) {
+            mLauncher.getTaskbarController().onHotseatUpdated();
         }
     }
 
+    private void removeOutlineDrawings() {
+        if (mOutlineDrawings.isEmpty()) return;
+        for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
+            mHotseat.removeDelegatedCellDrawing(outlineDrawing);
+        }
+        mHotseat.invalidate();
+        mOutlineDrawings.clear();
+    }
+
+
     /**
      * Unregisters callbacks and frees resources
      */
@@ -242,7 +288,9 @@
      * start and pauses predicted apps update on the hotseat
      */
     public void setPauseUIUpdate(boolean paused) {
-        mUIUpdatePaused = paused;
+        mPauseFlags = paused
+                ? (mPauseFlags | FLAG_UPDATE_PAUSED)
+                : (mPauseFlags & ~FLAG_UPDATE_PAUSED);
         if (!paused) {
             fillGapsWithPrediction();
         }
@@ -330,7 +378,7 @@
                 continue;
             }
             if (dragObject.dragSource == this && icon.equals(dragObject.originalView)) {
-                mHotseat.removeView(icon);
+                removeIconWithoutNotify(icon);
                 continue;
             }
             int rank = ((WorkspaceItemInfo) icon.getTag()).rank;
@@ -342,7 +390,7 @@
                 @Override
                 public void onAnimationSuccess(Animator animator) {
                     if (icon.getParent() != null) {
-                        mHotseat.removeView(icon);
+                        removeIconWithoutNotify(icon);
                     }
                 }
             });
@@ -351,6 +399,17 @@
         mIconRemoveAnimators.start();
     }
 
+    /**
+     * Removes icon while suppressing any extra tasks performed on view-hierarchy changes.
+     * This avoids recursive/redundant updates as the control updates the UI anyway after
+     * it's animation.
+     */
+    private void removeIconWithoutNotify(PredictedAppIcon icon) {
+        mPauseFlags |= FLAG_REMOVING_PREDICTED_ICON;
+        mHotseat.removeView(icon);
+        mPauseFlags &= ~FLAG_REMOVING_PREDICTED_ICON;
+    }
+
     @Override
     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
         removePredictedApps(mOutlineDrawings, dragObject);
@@ -358,14 +417,14 @@
         for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
             mHotseat.addDelegatedCellDrawing(outlineDrawing);
         }
-        mDragInProgress = true;
+        mPauseFlags |= FLAG_DRAG_IN_PROGRESS;
         mHotseat.invalidate();
     }
 
     @Override
     public void onDragEnd() {
-        mDragInProgress = false;
-        fillGapsWithPrediction(true, this::removeOutlineDrawings);
+        mPauseFlags &= ~FLAG_DRAG_IN_PROGRESS;
+        fillGapsWithPrediction(true);
     }
 
     @Nullable
@@ -386,15 +445,6 @@
         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;
@@ -409,16 +459,6 @@
      * Logs rank info based on current list of predicted items
      */
     public void logLaunchedAppRankingInfo(@NonNull ItemInfo itemInfo, InstanceId instanceId) {
-        if (Utilities.IS_DEBUG_DEVICE) {
-            final String pkg = itemInfo.getTargetComponent() != null
-                    ? itemInfo.getTargetComponent().getPackageName() : "unknown";
-            HotseatFileLog.INSTANCE.get(mLauncher).log("UserEvent",
-                    "appLaunch: packageName:" + pkg + ",isWorkApp:" + (itemInfo.user != null
-                            && !Process.myUserHandle().equals(itemInfo.user))
-                            + ",launchLocation:" + itemInfo.container);
-        }
-
-
         ComponentName targetCN = itemInfo.getTargetComponent();
         if (targetCN == null) {
             return;
diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
index c3f5c00..b1b4d70 100644
--- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
+++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
 import static com.android.launcher3.Utilities.getDevicePrefs;
@@ -80,11 +81,13 @@
             new PredictorState(CONTAINER_PREDICTION, "all_apps_predictions");
     private final PredictorState mHotseatState =
             new PredictorState(CONTAINER_HOTSEAT_PREDICTION, "hotseat_predictions");
+    private final PredictorState mWidgetsRecommendationState =
+            new PredictorState(CONTAINER_WIDGETS_PREDICTION, "widgets_prediction");
 
     private final InvariantDeviceProfile mIDP;
     private final AppEventProducer mAppEventProducer;
 
-    private boolean mActive = false;
+    protected boolean mActive = false;
 
     public QuickstepModelDelegate(Context context) {
         mAppEventProducer = new AppEventProducer(context, this::onAppTargetEvent);
@@ -111,6 +114,9 @@
         mHotseatState.items.setItems(
                 mHotseatState.storage.read(mApp.getContext(), hotseatFactory, ums.allUsers::get));
         mDataModel.extraItems.put(CONTAINER_HOTSEAT_PREDICTION, mHotseatState.items);
+
+        // Widgets prediction isn't used frequently. And thus, it is not persisted on disk.
+        mDataModel.extraItems.put(CONTAINER_WIDGETS_PREDICTION, mWidgetsRecommendationState.items);
         mActive = true;
     }
 
@@ -148,16 +154,22 @@
                         ? (FolderInfo) itemsIdMap.get(info.container) : null;
                 StatsLogCompatManager.writeSnapshot(info.buildProto(parent), instanceId);
             }
+            additionalSnapshotEvents(instanceId);
             prefs.edit().putLong(LAST_SNAPSHOT_TIME_MILLIS, now).apply();
         }
     }
 
+    protected void additionalSnapshotEvents(InstanceId snapshotInstanceId){}
+
     @Override
     public void validateData() {
         super.validateData();
         if (mAllAppsState.predictor != null) {
             mAllAppsState.predictor.requestPredictionUpdate();
         }
+        if (mWidgetsRecommendationState.predictor != null) {
+            mWidgetsRecommendationState.predictor.requestPredictionUpdate();
+        }
     }
 
     @Override
@@ -173,6 +185,7 @@
     private void destroyPredictors() {
         mAllAppsState.destroyPredictor();
         mHotseatState.destroyPredictor();
+        mWidgetsRecommendationState.destroyPredictor();
     }
 
     @WorkerThread
@@ -201,6 +214,11 @@
                         .setExtras(convertDataModelToAppTargetBundle(context, mDataModel))
                         .build()));
 
+        registerWidgetsPredictor(apm.createAppPredictionSession(
+                new AppPredictionContext.Builder(context)
+                        .setUiSurface("widgets")
+                        .setPredictedTargetCount(mIDP.numColumns)
+                        .build()));
     }
 
     private void registerPredictor(PredictorState state, AppPredictor predictor) {
@@ -218,6 +236,20 @@
         mApp.getModel().enqueueModelUpdateTask(new PredictionUpdateTask(state, targets));
     }
 
+    private void registerWidgetsPredictor(AppPredictor predictor) {
+        mWidgetsRecommendationState.predictor = predictor;
+        mWidgetsRecommendationState.predictor.registerPredictionUpdates(
+                Executors.MODEL_EXECUTOR, targets -> {
+                    if (mWidgetsRecommendationState.setTargets(targets)) {
+                        // No diff, skip
+                        return;
+                    }
+                    mApp.getModel().enqueueModelUpdateTask(
+                            new WidgetsPredictionUpdateTask(mWidgetsRecommendationState, targets));
+                });
+        mWidgetsRecommendationState.predictor.requestPredictionUpdate();
+    }
+
     @Override
     public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
         if ((changeFlags & CHANGE_FLAG_GRID) != 0) {
@@ -236,14 +268,14 @@
     static class PredictorState {
 
         public final FixedContainerItems items;
-        public final PersistedItemArray storage;
+        public final PersistedItemArray<ItemInfo> storage;
         public AppPredictor predictor;
 
         private List<AppTarget> mLastTargets;
 
         PredictorState(int container, String storageName) {
             items = new FixedContainerItems(container);
-            storage = new PersistedItemArray(storageName);
+            storage = new PersistedItemArray<>(storageName);
             mLastTargets = Collections.emptyList();
         }
 
@@ -255,7 +287,7 @@
         }
 
         /**
-         * Sets the new targets and returns true if it was different than before.
+         * Sets the new targets and returns true if it was the same as before.
          */
         boolean setTargets(List<AppTarget> newTargets) {
             List<AppTarget> oldTargets = mLastTargets;
@@ -289,7 +321,7 @@
         return true;
     }
 
-    private static class WorkspaceItemFactory implements PersistedItemArray.ItemFactory {
+    private static class WorkspaceItemFactory implements PersistedItemArray.ItemFactory<ItemInfo> {
 
         private final LauncherAppState mAppState;
         private final UserManagerState mUMS;
diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
new file mode 100644
index 0000000..a29ac1a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2021 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 android.app.prediction.AppTarget;
+
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/** Task to update model as a result of predicted widgets update */
+public final class WidgetsPredictionUpdateTask extends BaseModelUpdateTask {
+    private final PredictorState mPredictorState;
+    private final List<AppTarget> mTargets;
+
+    WidgetsPredictionUpdateTask(PredictorState predictorState, List<AppTarget> targets) {
+        mPredictorState = predictorState;
+        mTargets = targets;
+    }
+
+    /**
+     * Uses the app predication result to infer widgets that the user may want to use.
+     *
+     * <p>The algorithm uses the app prediction ranking to create a widgets ranking which only
+     * includes one widget per app and excludes widgets that have already been added to the
+     * workspace.
+     */
+    @Override
+    public void execute(LauncherAppState appState, BgDataModel dataModel, AllAppsList apps) {
+        Set<ComponentKey> widgetsInWorkspace = dataModel.appWidgets.stream().map(
+                widget -> new ComponentKey(widget.providerName, widget.user)).collect(
+                Collectors.toSet());
+        Map<PackageUserKey, List<WidgetItem>> allWidgets =
+                dataModel.widgetsModel.getAllWidgetsWithoutShortcuts();
+
+        ArrayList<ItemInfo> recommendedWidgetsInDescendingOrder = new ArrayList<>();
+        for (AppTarget app : mTargets) {
+            PackageUserKey packageUserKey = new PackageUserKey(app.getPackageName(), app.getUser());
+            if (allWidgets.containsKey(packageUserKey)) {
+                List<WidgetItem> notAddedWidgets = allWidgets.get(packageUserKey).stream()
+                        .filter(item ->
+                                !widgetsInWorkspace.contains(
+                                        new ComponentKey(item.componentName, item.user)))
+                        .collect(Collectors.toList());
+                if (notAddedWidgets.size() > 0) {
+                    // Even an apps have more than one widgets, we only include one widget.
+                    recommendedWidgetsInDescendingOrder.add(
+                            new PendingAddWidgetInfo(notAddedWidgets.get(0).widgetInfo));
+                }
+            }
+        }
+        FixedContainerItems fixedContainerItems = mPredictorState.items;
+        fixedContainerItems.items.clear();
+        fixedContainerItems.items.addAll(recommendedWidgetsInDescendingOrder);
+        bindExtraContainerItems(fixedContainerItems);
+
+        // Don't store widgets prediction to disk because it is not used frequently.
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java b/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java
index e302b4f..4d7cc85 100644
--- a/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java
+++ b/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.proxy;
 
 import android.app.Activity;
+import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentSender.SendIntentException;
@@ -48,19 +49,20 @@
             return;
         }
 
-        if (mParams.intent != null) {
-            startActivityForResult(mParams.intent, mParams.requestCode, mParams.options);
-            return;
-        } else if (mParams.intentSender != null) {
-            try {
+        try {
+            if (mParams.intent != null) {
+                startActivityForResult(mParams.intent, mParams.requestCode, mParams.options);
+                return;
+            } else if (mParams.intentSender != null) {
                 startIntentSenderForResult(mParams.intentSender, mParams.requestCode,
                         mParams.fillInIntent, mParams.flagsMask, mParams.flagsValues,
                         mParams.extraFlags,
                         mParams.options);
                 return;
-            } catch (SendIntentException e) {
-                mParams.deliverResult(this, RESULT_CANCELED, null);
             }
+        } catch (NullPointerException | ActivityNotFoundException | SecurityException
+                | SendIntentException e) {
+            mParams.deliverResult(this, RESULT_CANCELED, null);
         }
         finishAndRemoveTask();
     }
diff --git a/quickstep/src/com/android/launcher3/search/DeviceSearchAdapterProvider.java b/quickstep/src/com/android/launcher3/search/DeviceSearchAdapterProvider.java
deleted file mode 100644
index 02be0b9..0000000
--- a/quickstep/src/com/android/launcher3/search/DeviceSearchAdapterProvider.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2021 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.search;
-
-import static com.android.launcher3.allapps.AllAppsGridAdapter.VIEW_TYPE_ICON;
-
-import android.app.search.SearchTarget;
-import android.util.Log;
-import android.util.SparseIntArray;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.app.search.LayoutType;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.allapps.AllAppsContainerView;
-import com.android.launcher3.allapps.AllAppsGridAdapter;
-import com.android.launcher3.allapps.search.SearchAdapterProvider;
-
-/**
- * Provides views for on-device search results
- */
-public class DeviceSearchAdapterProvider extends SearchAdapterProvider {
-
-    public static final int VIEW_TYPE_SEARCH_CORPUS_TITLE = 1 << 5;
-    public static final int VIEW_TYPE_SEARCH_SLICE = 1 << 7;
-    public static final int VIEW_TYPE_SEARCH_ICON = (1 << 8) | VIEW_TYPE_ICON;
-    public static final int VIEW_TYPE_SEARCH_ICON_ROW = (1 << 9);
-    public static final int VIEW_TYPE_SEARCH_PEOPLE = 1 << 11;
-    public static final int VIEW_TYPE_SEARCH_THUMBNAIL = 1 << 12;
-    public static final int VIEW_TYPE_SEARCH_WIDGET_LIVE = 1 << 15;
-    public static final int VIEW_TYPE_SEARCH_WIDGET_PREVIEW = 1 << 16;
-
-    private static final String TAG = "SearchServiceAdapterProvider";
-
-    private final AllAppsContainerView mAppsView;
-    private final SparseIntArray mViewTypeToLayoutMap = new SparseIntArray();
-
-    public DeviceSearchAdapterProvider(Launcher launcher, AllAppsContainerView appsView) {
-        super(launcher);
-        mAppsView = appsView;
-
-        mViewTypeToLayoutMap.put(VIEW_TYPE_SEARCH_CORPUS_TITLE, R.layout.search_section_title);
-        mViewTypeToLayoutMap.put(VIEW_TYPE_SEARCH_ICON, R.layout.search_result_icon);
-        mViewTypeToLayoutMap.put(VIEW_TYPE_SEARCH_ICON_ROW, R.layout.search_result_icon_row);
-        mViewTypeToLayoutMap.put(VIEW_TYPE_SEARCH_SLICE, R.layout.search_result_slice);
-        mViewTypeToLayoutMap.put(VIEW_TYPE_SEARCH_PEOPLE, R.layout.search_result_people_item);
-        mViewTypeToLayoutMap.put(VIEW_TYPE_SEARCH_THUMBNAIL, R.layout.search_result_thumbnail);
-        mViewTypeToLayoutMap.put(VIEW_TYPE_SEARCH_WIDGET_LIVE, R.layout.search_result_widget_live);
-        mViewTypeToLayoutMap.put(VIEW_TYPE_SEARCH_WIDGET_PREVIEW,
-                R.layout.search_result_widget_preview);
-    }
-
-    @Override
-    public void onBindView(AllAppsGridAdapter.ViewHolder holder, int position) {
-        SearchAdapterItem item = (SearchAdapterItem) mAppsView.getApps().getAdapterItems().get(
-                position);
-        SearchTargetHandler
-                payloadResultView =
-                (SearchTargetHandler) holder.itemView;
-        payloadResultView.apply(item.getSearchTarget(), item.getInlineItems());
-    }
-
-    @Override
-    public boolean isSearchView(int viewType) {
-        return mViewTypeToLayoutMap.get(viewType, -1) != -1;
-    }
-
-    @Override
-    public AllAppsGridAdapter.ViewHolder onCreateViewHolder(LayoutInflater inflater,
-            ViewGroup parent, int viewType) {
-        return new AllAppsGridAdapter.ViewHolder(inflater.inflate(
-                mViewTypeToLayoutMap.get(viewType), parent, false));
-    }
-
-    @Override
-    public int getGridSpanSize(int viewType, int appsPerRow) {
-        if (viewType == VIEW_TYPE_SEARCH_THUMBNAIL
-                || viewType == VIEW_TYPE_SEARCH_WIDGET_PREVIEW) {
-            return appsPerRow;
-        }
-        return super.getGridSpanSize(viewType, appsPerRow);
-    }
-
-
-    @Override
-    public boolean onAdapterItemSelected(AllAppsGridAdapter.AdapterItem adapterItem, View view) {
-        if (view instanceof SearchTargetHandler) {
-            return ((SearchTargetHandler) view).quickSelect();
-        }
-        return false;
-    }
-
-    /**
-     * Determines what view type should be used to present search target.
-     * Returns -1 if viewType is not found or if required field is not present
-     * to render the viewType.
-     */
-    public int getViewTypeForSearchTarget(SearchTarget t) {
-        switch (t.getLayoutType()) {
-            case LayoutType.TEXT_HEADER:
-                return VIEW_TYPE_SEARCH_CORPUS_TITLE;
-            case LayoutType.ICON_SINGLE_VERTICAL_TEXT:
-                return VIEW_TYPE_SEARCH_ICON;
-            case LayoutType.ICON_SLICE:
-                if (t.getSliceUri() != null) {
-                    return VIEW_TYPE_SEARCH_SLICE;
-                }
-                Log.w(TAG, "Dropping as LayoutType.ICON_SLICE target doesn't contain sliceUri.");
-                break;
-            case LayoutType.ICON_DOUBLE_HORIZONTAL_TEXT:
-            case LayoutType.ICON_SINGLE_HORIZONTAL_TEXT:
-                return VIEW_TYPE_SEARCH_ICON_ROW;
-            case LayoutType.THUMBNAIL:
-                if (t.getSearchAction() != null) {
-                    return VIEW_TYPE_SEARCH_THUMBNAIL;
-                }
-                Log.w(TAG, "Dropping as LayoutType.THUMBNAIL target doesn't contain searchAction.");
-                break;
-            case LayoutType.WIDGET_PREVIEW:
-                return VIEW_TYPE_SEARCH_WIDGET_PREVIEW;
-            case LayoutType.WIDGET_LIVE:
-                return VIEW_TYPE_SEARCH_WIDGET_LIVE;
-        }
-
-        return -1;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/search/DeviceSearchEdu.java b/quickstep/src/com/android/launcher3/search/DeviceSearchEdu.java
deleted file mode 100644
index 425e557..0000000
--- a/quickstep/src/com/android/launcher3/search/DeviceSearchEdu.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright (C) 2021 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.search;
-
-import static com.android.launcher3.util.OnboardingPrefs.SEARCH_EDU_SEEN;
-
-import android.animation.PropertyValuesHolder;
-import android.content.Context;
-import android.graphics.Rect;
-import android.text.Editable;
-import android.text.TextWatcher;
-import android.util.AttributeSet;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.EditText;
-import android.widget.TextView;
-
-import androidx.core.graphics.ColorUtils;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Insettable;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.util.Themes;
-import com.android.launcher3.views.AbstractSlideInView;
-
-/**
- * Feature education for on-device Search. Shown the first time user opens AllApps Search
- */
-public class DeviceSearchEdu extends AbstractSlideInView implements
-        StateManager.StateListener<LauncherState>, TextWatcher, Insettable,
-        TextView.OnEditorActionListener {
-
-    private static final long ANIMATION_DURATION = 350;
-    private static final int ANIMATION_CONTENT_TRANSLATION = 200;
-
-    private EditText mEduInput;
-
-    private View mInputWrapper;
-    private EditText mSearchInput;
-
-    private boolean mSwitchFocusOnDismiss;
-
-
-    public DeviceSearchEdu(Context context) {
-        this(context, null, 0);
-    }
-
-    public DeviceSearchEdu(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public DeviceSearchEdu(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-    }
-
-
-    private void close(boolean animate, boolean markAsSeen) {
-        handleClose(animate);
-        if (markAsSeen) {
-            mLauncher.getOnboardingPrefs().markChecked(SEARCH_EDU_SEEN);
-        }
-    }
-
-    @Override
-    protected void handleClose(boolean animate) {
-        handleClose(animate, ANIMATION_DURATION);
-        mLauncher.getStateManager().removeStateListener(this);
-    }
-
-    @Override
-    protected boolean isOfType(int type) {
-        return false;
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mSearchInput = mLauncher.getAppsView().getSearchUiManager().getEditText();
-        mInputWrapper = findViewById(R.id.search_box_wrapper);
-        mContent = findViewById(R.id.edu_wrapper);
-
-        mEduInput = findViewById(R.id.mock_search_box);
-        mEduInput.setHint(R.string.all_apps_on_device_search_bar_hint);
-        mEduInput.addTextChangedListener(this);
-        if (mSearchInput != null) {
-            mEduInput.getLayoutParams().height = mSearchInput.getHeight();
-            mEduInput.setOnEditorActionListener(this);
-        } else {
-            mEduInput.setVisibility(INVISIBLE);
-        }
-
-        findViewById(R.id.dismiss_edu).setOnClickListener((view) -> {
-            mSwitchFocusOnDismiss = true;
-            close(true, true);
-        });
-    }
-
-    private void showInternal() {
-        mLauncher.getStateManager().addStateListener(this);
-        AbstractFloatingView.closeAllOpenViews(mLauncher);
-        attachToContainer();
-        if (mSearchInput != null) {
-            Rect r = mLauncher.getViewBounds(mSearchInput);
-            mEduInput.requestFocus();
-            InputMethodManager imm = mLauncher.getSystemService(InputMethodManager.class);
-            imm.showSoftInput(mEduInput, InputMethodManager.SHOW_IMPLICIT);
-            ((LayoutParams) mInputWrapper.getLayoutParams()).setMargins(0, r.top, 0, 0);
-        }
-        animateOpen();
-    }
-
-    @Override
-    protected int getScrimColor(Context context) {
-        return ColorUtils.setAlphaComponent(Themes.getAttrColor(context, R.attr.allAppsScrimColor),
-                230);
-    }
-
-    protected void setTranslationShift(float translationShift) {
-        mTranslationShift = translationShift;
-        mContent.setAlpha(getBoxedProgress(1 - mTranslationShift, .25f, 1));
-        mContent.setTranslationY(ANIMATION_CONTENT_TRANSLATION * translationShift);
-        if (mColorScrim != null) {
-            mColorScrim.setAlpha(getBoxedProgress(1 - mTranslationShift, 0, .75f));
-        }
-    }
-
-    /**
-     * Given input [0-1], returns progress within bounds [min,max] allowing for staged animations
-     */
-    private float getBoxedProgress(float input, float min, float max) {
-        if (input < min) return 0;
-        if (input > max) return 1;
-        return (input - min) / (max - min);
-    }
-
-    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.setDuration(ANIMATION_DURATION);
-        mOpenCloseAnimator.start();
-    }
-
-    /**
-     * Show On-device search education view.
-     */
-    public static void show(Launcher launcher) {
-        LayoutInflater layoutInflater = LayoutInflater.from(launcher);
-        ((DeviceSearchEdu) layoutInflater.inflate(
-                R.layout.search_edu_view, launcher.getDragLayer(),
-                false)).showInternal();
-    }
-
-    @Override
-    public void onStateTransitionStart(LauncherState toState) {
-        close(true, false);
-    }
-
-    @Override
-    protected void onCloseComplete() {
-        super.onCloseComplete();
-        if (mSearchInput != null && mSwitchFocusOnDismiss) {
-            mSearchInput.requestFocus();
-            mSearchInput.setSelection(mSearchInput.getText().length());
-        }
-    }
-
-    @Override
-    public void afterTextChanged(Editable editable) {
-        //Does nothing
-    }
-
-    @Override
-    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
-        //Does nothing
-    }
-
-    @Override
-    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
-        if (mSearchInput != null) {
-            mSearchInput.setText(charSequence.toString());
-            mSwitchFocusOnDismiss = true;
-            close(true, true);
-        }
-    }
-
-    @Override
-    public void setInsets(Rect insets) {
-
-    }
-
-    @Override
-    public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
-        mSearchInput.onEditorAction(i);
-        close(true, true);
-        return true;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/search/FallbackSearchInputView.java b/quickstep/src/com/android/launcher3/search/FallbackSearchInputView.java
deleted file mode 100644
index 7b10622..0000000
--- a/quickstep/src/com/android/launcher3/search/FallbackSearchInputView.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2021 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.search;
-
-import static com.android.launcher3.LauncherState.ALL_APPS;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-
-import com.android.launcher3.ExtendedEditText;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.allapps.AllAppsContainerView;
-import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
-import com.android.launcher3.allapps.AllAppsStore;
-import com.android.launcher3.allapps.AlphabeticalAppsList;
-import com.android.launcher3.allapps.FloatingHeaderView;
-import com.android.launcher3.allapps.search.AllAppsSearchBarController;
-import com.android.launcher3.allapps.search.SearchAlgorithm;
-
-import java.util.ArrayList;
-
-/**
- * A search view shown in all apps for on device search
- */
-public class FallbackSearchInputView extends ExtendedEditText
-        implements AllAppsSearchBarController.Callbacks, AllAppsStore.OnUpdateListener {
-
-    private final AllAppsSearchBarController mSearchBarController;
-
-    private AlphabeticalAppsList mApps;
-    private Runnable mOnResultsChanged;
-    private AllAppsContainerView mAppsView;
-
-    public FallbackSearchInputView(Context context) {
-        this(context, null);
-    }
-
-    public FallbackSearchInputView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public FallbackSearchInputView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        mSearchBarController = new AllAppsSearchBarController();
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        Launcher.getLauncher(getContext()).getAppsView().getAppsStore().addUpdateListener(this);
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        Launcher.getLauncher(getContext()).getAppsView().getAppsStore().removeUpdateListener(this);
-    }
-
-    /**
-     * Initializes SearchInput
-     */
-    public void initialize(AllAppsContainerView appsView, SearchAlgorithm algo, Runnable changed) {
-        mOnResultsChanged = changed;
-        mApps = appsView.getApps();
-        mAppsView = appsView;
-        mSearchBarController.initialize(algo, this, Launcher.getLauncher(getContext()), this);
-    }
-
-    @Override
-    public void onSearchResult(String query, ArrayList<AdapterItem> items) {
-        if (mApps != null && getParent() != null) {
-            mApps.setSearchResults(items);
-            notifyResultChanged();
-            collapseAppsViewHeader(true);
-            mAppsView.setLastSearchQuery(query);
-        }
-    }
-
-    @Override
-    public void onAppendSearchResult(String query, ArrayList<AdapterItem> items) {
-        if (mApps != null && getParent() != null) {
-            mApps.appendSearchResults(items);
-            notifyResultChanged();
-        }
-    }
-
-    @Override
-    public void clearSearchResult() {
-        if (getParent() != null && mApps != null) {
-            mApps.setSearchResults(null);
-            notifyResultChanged();
-            collapseAppsViewHeader(false);
-            mAppsView.onClearSearchResult();
-        }
-    }
-
-    @Override
-    public void onAppsUpdated() {
-        mSearchBarController.refreshSearchResult();
-    }
-
-    private void collapseAppsViewHeader(boolean collapse) {
-        FloatingHeaderView header = mAppsView.getFloatingHeaderView();
-        if (header != null) {
-            header.setCollapsed(collapse);
-        }
-    }
-
-    private void notifyResultChanged() {
-        if (mOnResultsChanged != null) {
-            mOnResultsChanged.run();
-        }
-        mAppsView.onSearchResultsChanged();
-    }
-
-    @Override
-    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
-        // TODO: Consider animating the state transition here
-        if (focused) {
-            // Getting focus will open the keyboard. Go to the all-apps state, so that the input
-            // box is at the top and there is enough space below to show search results.
-            Launcher.getLauncher(getContext()).getStateManager().goToState(ALL_APPS, false);
-        }
-        super.onFocusChanged(focused, direction, previouslyFocusedRect);
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/search/SearchAdapterItem.java b/quickstep/src/com/android/launcher3/search/SearchAdapterItem.java
deleted file mode 100644
index 0eb3edb..0000000
--- a/quickstep/src/com/android/launcher3/search/SearchAdapterItem.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2021 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.search;
-
-import static com.android.launcher3.search.DeviceSearchAdapterProvider.VIEW_TYPE_SEARCH_ICON;
-import static com.android.launcher3.search.DeviceSearchAdapterProvider.VIEW_TYPE_SEARCH_ICON_ROW;
-import static com.android.launcher3.search.DeviceSearchAdapterProvider.VIEW_TYPE_SEARCH_PEOPLE;
-import static com.android.launcher3.search.DeviceSearchAdapterProvider.VIEW_TYPE_SEARCH_SLICE;
-import static com.android.launcher3.search.DeviceSearchAdapterProvider.VIEW_TYPE_SEARCH_THUMBNAIL;
-import static com.android.launcher3.search.DeviceSearchAdapterProvider.VIEW_TYPE_SEARCH_WIDGET_LIVE;
-import static com.android.launcher3.search.DeviceSearchAdapterProvider.VIEW_TYPE_SEARCH_WIDGET_PREVIEW;
-
-import android.app.search.SearchTarget;
-
-import com.android.launcher3.allapps.AllAppsGridAdapter;
-import com.android.systemui.plugins.shared.SearchTargetLegacy;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Extension of AdapterItem that contains an extra payload specific to item
- */
-public class SearchAdapterItem extends AllAppsGridAdapter.AdapterItem {
-    private SearchTargetLegacy mSearchTargetLegacy;
-    private SearchTarget mSearchTarget;
-    private List<SearchTarget> mInlineItems = new ArrayList<>();
-
-
-    private static final int AVAILABLE_FOR_ACCESSIBILITY =
-            VIEW_TYPE_SEARCH_SLICE | VIEW_TYPE_SEARCH_PEOPLE | VIEW_TYPE_SEARCH_THUMBNAIL
-                    | VIEW_TYPE_SEARCH_ICON_ROW | VIEW_TYPE_SEARCH_ICON
-                    | VIEW_TYPE_SEARCH_WIDGET_PREVIEW | VIEW_TYPE_SEARCH_WIDGET_LIVE;
-
-    public SearchAdapterItem(SearchTargetLegacy searchTargetLegacy, int type) {
-        mSearchTargetLegacy = searchTargetLegacy;
-        viewType = type;
-    }
-
-    public SearchAdapterItem(SearchTarget searchTarget, int type) {
-        mSearchTarget = searchTarget;
-        viewType = type;
-    }
-
-    public SearchTargetLegacy getSearchTargetLegacy() {
-        return mSearchTargetLegacy;
-    }
-
-    public SearchTarget getSearchTarget() {
-        return mSearchTarget;
-    }
-
-    public List<SearchTarget> getInlineItems() {
-        return mInlineItems;
-    }
-    @Override
-    protected boolean isCountedForAccessibility() {
-        return (AVAILABLE_FOR_ACCESSIBILITY & viewType) == viewType;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/search/SearchResultIcon.java b/quickstep/src/com/android/launcher3/search/SearchResultIcon.java
deleted file mode 100644
index 3d89ac5..0000000
--- a/quickstep/src/com/android/launcher3/search/SearchResultIcon.java
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.search;
-
-import static com.android.launcher3.model.data.SearchActionItemInfo.FLAG_BADGE_FROM_ICON;
-import static com.android.launcher3.model.data.SearchActionItemInfo.FLAG_PRIMARY_ICON_FROM_TITLE;
-import static com.android.launcher3.search.SearchTargetUtil.BUNDLE_EXTRA_BADGE_FROM_ICON;
-import static com.android.launcher3.search.SearchTargetUtil.BUNDLE_EXTRA_PRIMARY_ICON_FROM_TITLE;
-import static com.android.launcher3.search.SearchTargetUtil.BUNDLE_EXTRA_SHOULD_START;
-import static com.android.launcher3.search.SearchTargetUtil.BUNDLE_EXTRA_SHOULD_START_FOR_RESULT;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-
-import android.app.search.SearchAction;
-import android.app.search.SearchTarget;
-import android.app.search.SearchTargetEvent;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.ShortcutInfo;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.UserHandle;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.app.search.ResultType;
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.allapps.AllAppsStore;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.ItemInfoWithIcon;
-import com.android.launcher3.model.data.PackageItemInfo;
-import com.android.launcher3.model.data.SearchActionItemInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.touch.ItemClickHandler;
-import com.android.launcher3.touch.ItemLongClickListener;
-import com.android.launcher3.util.ComponentKey;
-
-import java.util.List;
-import java.util.function.Consumer;
-
-/**
- * A {@link BubbleTextView} representing a single cell result in AllApps
- */
-public class SearchResultIcon extends BubbleTextView implements SearchTargetHandler {
-
-    private final Launcher mLauncher;
-
-    private String mTargetId;
-    private Consumer<ItemInfoWithIcon> mOnItemInfoChanged;
-
-
-    public SearchResultIcon(Context context) {
-        this(context, null, 0);
-    }
-
-    public SearchResultIcon(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public SearchResultIcon(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-        mLauncher = Launcher.getLauncher(getContext());
-    }
-
-    private boolean mLongPressSupported;
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        setLongPressTimeoutFactor(1f);
-        setOnFocusChangeListener(mLauncher.getFocusHandler());
-        setOnClickListener(this);
-        setOnLongClickListener(this);
-        setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                mLauncher.getDeviceProfile().allAppsCellHeightPx));
-    }
-
-    /**
-     * Applies {@link SearchTarget} to view. registers a consumer after a corresponding
-     * {@link ItemInfoWithIcon} is created
-     */
-    public void applySearchTarget(SearchTarget searchTarget, List<SearchTarget> inlineItems,
-            Consumer<ItemInfoWithIcon> cb) {
-        mOnItemInfoChanged = cb;
-        apply(searchTarget, inlineItems);
-    }
-
-    @Override
-    public void apply(SearchTarget parentTarget, List<SearchTarget> children) {
-        mTargetId = parentTarget.getId();
-        if (parentTarget.getShortcutInfo() != null) {
-            prepareUsingShortcutInfo(parentTarget.getShortcutInfo());
-            mLongPressSupported = true;
-        } else if (parentTarget.getSearchAction() != null) {
-            prepareUsingSearchAction(parentTarget);
-            mLongPressSupported = false;
-        } else {
-            prepareUsingApp(new ComponentName(parentTarget.getPackageName(),
-                    parentTarget.getExtras().getString(SearchTargetUtil.EXTRA_CLASS)),
-                    parentTarget.getUserHandle());
-            mLongPressSupported = true;
-        }
-    }
-
-    private void prepareUsingSearchAction(SearchTarget searchTarget) {
-        SearchAction searchAction = searchTarget.getSearchAction();
-        Bundle extras = searchAction.getExtras();
-
-        SearchActionItemInfo itemInfo = new SearchActionItemInfo(searchAction.getIcon(),
-                searchTarget.getPackageName(), searchTarget.getUserHandle(),
-                searchAction.getTitle()
-        );
-        itemInfo.setIntent(searchAction.getIntent());
-        itemInfo.setPendingIntent(searchAction.getPendingIntent());
-
-        //TODO: remove this after flags are introduced in SearchAction. Settings results require
-        // startActivityForResult
-        boolean isSettingsResult = searchTarget.getResultType() == ResultType.SETTING;
-        if ((extras != null && extras.getBoolean(BUNDLE_EXTRA_SHOULD_START_FOR_RESULT))
-                || isSettingsResult) {
-            itemInfo.setFlags(SearchActionItemInfo.FLAG_SHOULD_START_FOR_RESULT);
-        } else if (extras != null && extras.getBoolean(BUNDLE_EXTRA_SHOULD_START)) {
-            itemInfo.setFlags(SearchActionItemInfo.FLAG_SHOULD_START);
-        }
-        if (extras != null && extras.getBoolean(BUNDLE_EXTRA_BADGE_FROM_ICON)) {
-            itemInfo.setFlags(FLAG_BADGE_FROM_ICON);
-        }
-        if (extras != null && extras.getBoolean(BUNDLE_EXTRA_PRIMARY_ICON_FROM_TITLE)) {
-            itemInfo.setFlags(FLAG_PRIMARY_ICON_FROM_TITLE);
-        }
-
-        notifyItemInfoChanged(itemInfo);
-        LauncherAppState appState = LauncherAppState.getInstance(mLauncher);
-        MODEL_EXECUTOR.post(() -> {
-            try (LauncherIcons li = LauncherIcons.obtain(getContext())) {
-                Icon icon = searchTarget.getSearchAction().getIcon();
-                Drawable d;
-                // This bitmapInfo can be used as main icon or as a badge
-                BitmapInfo bitmapInfo;
-                if (icon == null) {
-                    PackageItemInfo pkgInfo = new PackageItemInfo(searchTarget.getPackageName());
-                    pkgInfo.user = searchTarget.getUserHandle();
-                    appState.getIconCache().getTitleAndIconForApp(pkgInfo, false);
-                    bitmapInfo = pkgInfo.bitmap;
-                } else {
-                    d = itemInfo.getIcon().loadDrawable(getContext());
-                    bitmapInfo = li.createBadgedIconBitmap(d, itemInfo.user,
-                            Build.VERSION.SDK_INT);
-                }
-
-                BitmapInfo bitmapMainIcon;
-                if (itemInfo.hasFlags(FLAG_PRIMARY_ICON_FROM_TITLE)) {
-                    bitmapMainIcon = li.createIconBitmap(
-                            String.valueOf(itemInfo.title.charAt(0)),
-                            bitmapInfo.color);
-                } else {
-                    bitmapMainIcon = bitmapInfo;
-                }
-                if (itemInfo.hasFlags(FLAG_BADGE_FROM_ICON)) {
-                    itemInfo.bitmap = li.badgeBitmap(bitmapMainIcon.icon, bitmapInfo);
-                } else {
-                    itemInfo.bitmap = bitmapInfo;
-                }
-
-            }
-            MAIN_EXECUTOR.post(() -> applyFromSearchActionItemInfo(itemInfo));
-        });
-    }
-
-    private void prepareUsingApp(ComponentName componentName, UserHandle userHandle) {
-        AllAppsStore appsStore = mLauncher.getAppsView().getAppsStore();
-        AppInfo appInfo = appsStore.getApp(new ComponentKey(componentName, userHandle));
-
-        if (appInfo == null) {
-            setVisibility(GONE);
-            return;
-        }
-        applyFromApplicationInfo(appInfo);
-        notifyItemInfoChanged(appInfo);
-    }
-
-
-    private void prepareUsingShortcutInfo(ShortcutInfo shortcutInfo) {
-        WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo(shortcutInfo, getContext());
-        notifyItemInfoChanged(workspaceItemInfo);
-        LauncherAppState launcherAppState = LauncherAppState.getInstance(getContext());
-        MODEL_EXECUTOR.execute(() -> {
-            launcherAppState.getIconCache().getShortcutIcon(workspaceItemInfo, shortcutInfo);
-            MAIN_EXECUTOR.post(() -> applyFromWorkspaceItem(workspaceItemInfo));
-        });
-    }
-
-    @Override
-    public boolean quickSelect() {
-        this.performClick();
-        notifyEvent(mLauncher, mTargetId, SearchTargetEvent.ACTION_LAUNCH_KEYBOARD_FOCUS);
-        return true;
-    }
-
-    @Override
-    public void onClick(View view) {
-        ItemClickHandler.INSTANCE.onClick(view);
-        notifyEvent(mLauncher, mTargetId, SearchTargetEvent.ACTION_LAUNCH_TOUCH);
-    }
-
-    @Override
-    public boolean onLongClick(View view) {
-        if (!mLongPressSupported) {
-            return false;
-        }
-        notifyEvent(mLauncher, mTargetId, SearchTargetEvent.ACTION_LONGPRESS);
-        return ItemLongClickListener.INSTANCE_ALL_APPS.onLongClick(this);
-    }
-
-
-
-    private void notifyItemInfoChanged(ItemInfoWithIcon itemInfoWithIcon) {
-        if (mOnItemInfoChanged != null) {
-            mOnItemInfoChanged.accept(itemInfoWithIcon);
-            mOnItemInfoChanged = null;
-        }
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/search/SearchResultIconRow.java b/quickstep/src/com/android/launcher3/search/SearchResultIconRow.java
deleted file mode 100644
index eafbc73..0000000
--- a/quickstep/src/com/android/launcher3/search/SearchResultIconRow.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.search;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-
-import android.app.search.SearchTarget;
-import android.content.Context;
-import android.content.pm.ShortcutInfo;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.R;
-import com.android.launcher3.model.data.ItemInfoWithIcon;
-import com.android.launcher3.model.data.PackageItemInfo;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A full width representation of {@link SearchResultIcon} with a secondary label and inline
- * SearchTargets
- */
-public class SearchResultIconRow extends LinearLayout implements SearchTargetHandler {
-
-    public static final int MAX_INLINE_ITEMS = 2;
-
-    protected final Launcher mLauncher;
-    private final LauncherAppState mLauncherAppState;
-    private SearchResultIcon mResultIcon;
-
-    private TextView mTitleView;
-    private TextView mSubTitleView;
-    private final SearchResultIcon[] mInlineIcons = new SearchResultIcon[MAX_INLINE_ITEMS];
-
-    private PackageItemInfo mProviderInfo;
-
-    public SearchResultIconRow(Context context) {
-        this(context, null, 0);
-    }
-
-    public SearchResultIconRow(Context context,
-            @Nullable AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public SearchResultIconRow(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        mLauncher = Launcher.getLauncher(getContext());
-        mLauncherAppState = LauncherAppState.getInstance(getContext());
-    }
-
-    protected int getIconSize() {
-        return mLauncher.getDeviceProfile().allAppsIconSizePx;
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-
-        int iconSize = getIconSize();
-
-        mResultIcon = findViewById(R.id.icon);
-        mTitleView = findViewById(R.id.title);
-        mSubTitleView = findViewById(R.id.subtitle);
-        mSubTitleView.setVisibility(GONE);
-        mResultIcon.getLayoutParams().height = iconSize;
-        mResultIcon.getLayoutParams().width = iconSize;
-        mResultIcon.setTextVisibility(false);
-
-        mInlineIcons[0] = findViewById(R.id.shortcut_0);
-        mInlineIcons[1] = findViewById(R.id.shortcut_1);
-        for (SearchResultIcon inlineIcon : mInlineIcons) {
-            inlineIcon.getLayoutParams().width = getIconSize();
-            // TODO: inlineIcon.setOnClickListener();
-        }
-
-        setOnClickListener(mResultIcon);
-        setOnLongClickListener(mResultIcon);
-    }
-
-    @Override
-    public void apply(SearchTarget parentTarget, List<SearchTarget> children) {
-        showSubtitleIfNeeded(null);
-        mResultIcon.applySearchTarget(parentTarget, children, this::onItemInfoCreated);
-        if (parentTarget.getShortcutInfo() != null) {
-            updateWithShortcutInfo(parentTarget.getShortcutInfo());
-        } else if (parentTarget.getSearchAction() != null) {
-            showSubtitleIfNeeded(parentTarget.getSearchAction().getSubtitle());
-        }
-        showInlineItems(children);
-    }
-
-    @Override
-    public boolean quickSelect() {
-        this.performClick();
-        return true;
-    }
-
-    private void updateWithShortcutInfo(ShortcutInfo shortcutInfo) {
-        PackageItemInfo packageItemInfo = new PackageItemInfo(shortcutInfo.getPackage());
-        if (packageItemInfo.equals(mProviderInfo)) return;
-        MODEL_EXECUTOR.post(() -> {
-            mLauncherAppState.getIconCache().getTitleAndIconForApp(packageItemInfo, true);
-            MAIN_EXECUTOR.post(() -> {
-                showSubtitleIfNeeded(packageItemInfo.title);
-                mProviderInfo = packageItemInfo;
-            });
-        });
-    }
-
-    protected void showSubtitleIfNeeded(CharSequence subTitle) {
-        if (!TextUtils.isEmpty(subTitle)) {
-            mSubTitleView.setText(subTitle);
-            mSubTitleView.setVisibility(VISIBLE);
-        } else {
-            mSubTitleView.setVisibility(GONE);
-        }
-    }
-
-    protected void showInlineItems(List<SearchTarget> children) {
-        for (int i = 0; i < MAX_INLINE_ITEMS; i++) {
-            if (i < children.size()) {
-                mInlineIcons[i].apply(children.get(0), new ArrayList<>());
-                mInlineIcons[i].setVisibility(VISIBLE);
-            } else {
-                mInlineIcons[i].setVisibility(GONE);
-            }
-        }
-    }
-
-    protected void onItemInfoCreated(ItemInfoWithIcon info) {
-        setTag(info);
-        mTitleView.setText(info.title);
-    }
-
-    @Override
-    public void onClick(View view) {
-        // do nothing.
-    }
-
-    @Override
-    public boolean onLongClick(View view) {
-        // do nothing.
-        return false;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/search/SearchResultIconSlice.java b/quickstep/src/com/android/launcher3/search/SearchResultIconSlice.java
deleted file mode 100644
index f6e5eba..0000000
--- a/quickstep/src/com/android/launcher3/search/SearchResultIconSlice.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.search;
-
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-
-import android.app.search.SearchTarget;
-import android.app.search.SearchTargetEvent;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.View;
-import android.widget.LinearLayout;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.lifecycle.LiveData;
-import androidx.slice.Slice;
-import androidx.slice.SliceItem;
-import androidx.slice.widget.EventInfo;
-import androidx.slice.widget.SliceView;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.R;
-import com.android.launcher3.model.data.PackageItemInfo;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A slice view wrapper with settings app icon at start
- */
-public class SearchResultIconSlice extends LinearLayout implements SearchTargetHandler,
-        SliceView.OnSliceActionListener {
-
-    private static final String TAG = "SearchSliceController";
-
-    private final Launcher mLauncher;
-
-    private SliceView mSliceView;
-    private SearchResultIcon mIcon;
-    private LiveData<Slice> mSliceLiveData;
-    private String mTargetId;
-
-    public SearchResultIconSlice(Context context) {
-        this(context, null, 0);
-    }
-
-    public SearchResultIconSlice(Context context,
-            @Nullable AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public SearchResultIconSlice(Context context, @Nullable AttributeSet attrs,
-            int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        mLauncher = Launcher.getLauncher(getContext());
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mSliceView = findViewById(R.id.slice);
-        mIcon = findViewById(R.id.icon);
-        mIcon.setTextVisibility(false);
-        int iconSize = mLauncher.getDeviceProfile().iconSizePx;
-        mIcon.getLayoutParams().height = iconSize;
-        mIcon.getLayoutParams().width = iconSize;
-    }
-
-    @Override
-    public void apply(SearchTarget parentTarget, List<SearchTarget> children) {
-        mTargetId = parentTarget.getId();
-        reset();
-        updateIcon(parentTarget, children);
-        try {
-            mSliceLiveData = mLauncher.getLiveSearchManager().getSliceForUri(
-                    parentTarget.getSliceUri());
-            mSliceLiveData.observe(mLauncher, mSliceView);
-        } catch (Exception ex) {
-            Log.e(TAG, "unable to bind slice", ex);
-        }
-    }
-
-    private void updateIcon(SearchTarget parentTarget, List<SearchTarget> children) {
-        if (children.size() == 1) {
-            mIcon.apply(children.get(0), new ArrayList<>());
-        } else {
-            LauncherAppState appState = LauncherAppState.getInstance(getContext());
-            MODEL_EXECUTOR.post(() -> {
-                PackageItemInfo pkgItem = new PackageItemInfo(parentTarget.getPackageName());
-                pkgItem.user = parentTarget.getUserHandle();
-                appState.getIconCache().getTitleAndIconForApp(pkgItem, false);
-                mIcon.applyFromItemInfoWithIcon(pkgItem);
-            });
-        }
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        mSliceView.setOnSliceActionListener(this);
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        reset();
-    }
-
-    private void reset() {
-        mSliceView.setOnSliceActionListener(null);
-        if (mSliceLiveData != null) {
-            mSliceLiveData.removeObservers(mLauncher);
-        }
-    }
-
-    @Override
-    public void onSliceAction(@NonNull EventInfo eventInfo, @NonNull SliceItem sliceItem) {
-        notifyEvent(mLauncher, mTargetId, SearchTargetEvent.ACTION_TAP);
-    }
-
-    @Override
-    public void onClick(View view) {
-        notifyEvent(mLauncher, mTargetId, SearchTargetEvent.ACTION_LONGPRESS);
-    }
-
-    @Override
-    public boolean onLongClick(View view) {
-        // do nothing
-        return false;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/search/SearchResultPeopleView.java b/quickstep/src/com/android/launcher3/search/SearchResultPeopleView.java
deleted file mode 100644
index 99e6aa5..0000000
--- a/quickstep/src/com/android/launcher3/search/SearchResultPeopleView.java
+++ /dev/null
@@ -1,199 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.search;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-
-import android.app.search.SearchTargetEvent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Process;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-import androidx.core.graphics.drawable.RoundedBitmapDrawable;
-import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.systemui.plugins.shared.SearchTargetLegacy;
-
-import java.util.ArrayList;
-
-/**
- * A view representing a single people search result in all apps
- */
-public class SearchResultPeopleView extends LinearLayout implements SearchTargetHandler {
-
-    public static final String TARGET_TYPE_PEOPLE = "people";
-
-    private final int mIconSize;
-    private final int mButtonSize;
-    private final PackageManager mPackageManager;
-    private View mIconView;
-    private TextView mTitleView;
-    private ImageButton[] mProviderButtons = new ImageButton[3];
-    private Intent mIntent;
-
-    private SearchTargetLegacy mSearchTarget;
-
-    public SearchResultPeopleView(Context context) {
-        this(context, null, 0);
-    }
-
-    public SearchResultPeopleView(Context context,
-            @Nullable AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public SearchResultPeopleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        DeviceProfile deviceProfile = Launcher.getLauncher(getContext()).getDeviceProfile();
-        mPackageManager = getContext().getPackageManager();
-        mIconSize = deviceProfile.iconSizePx;
-        mButtonSize = (int) (deviceProfile.iconSizePx / 1.5f);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mIconView = findViewById(R.id.icon);
-        mIconView.getLayoutParams().height = mIconSize;
-        mIconView.getLayoutParams().width = mIconSize;
-        mTitleView = findViewById(R.id.title);
-        mProviderButtons[0] = findViewById(R.id.provider_0);
-        mProviderButtons[1] = findViewById(R.id.provider_1);
-        mProviderButtons[2] = findViewById(R.id.provider_2);
-        for (ImageButton button : mProviderButtons) {
-            button.getLayoutParams().width = mButtonSize;
-            button.getLayoutParams().height = mButtonSize;
-        }
-    }
-
-    /*
-    @Override
-    public void applySearchTarget(SearchTargetLegacy searchTarget) {
-        mSearchTarget = searchTarget;
-        Bundle payload = searchTarget.getExtras();
-        mTitleView.setText(payload.getString("title"));
-        mIntent = payload.getParcelable("intent");
-        Bitmap contactIcon = payload.getParcelable("icon");
-        try (LauncherIcons li = LauncherIcons.obtain(getContext())) {
-            BitmapInfo badgeInfo = li.createBadgedIconBitmap(
-                    getAppIcon(mIntent.getPackage()), Process.myUserHandle(),
-                    Build.VERSION.SDK_INT);
-            setIcon(li.badgeBitmap(roundBitmap(contactIcon), badgeInfo).icon, false);
-        } catch (Exception e) {
-            setIcon(contactIcon, true);
-        }
-
-        ArrayList<Bundle> providers = payload.getParcelableArrayList("providers");
-        for (int i = 0; i < mProviderButtons.length; i++) {
-            ImageButton button = mProviderButtons[i];
-            if (providers != null && i < providers.size()) {
-                Bundle provider = providers.get(i);
-                Intent intent = provider.getParcelable("intent");
-                setupProviderButton(button, provider, intent);
-                UI_HELPER_EXECUTOR.post(() -> {
-                    String pkg = provider.getString("package_name");
-                    Drawable appIcon = getAppIcon(pkg);
-                    if (appIcon != null) {
-                        MAIN_EXECUTOR.post(() -> button.setImageDrawable(appIcon));
-                    }
-                });
-                button.setVisibility(VISIBLE);
-            } else {
-                button.setVisibility(GONE);
-            }
-        }
-    }
-    */
-
-    /**
-     * Normalizes the bitmap to look like rounded App Icon
-     * TODO(b/170234747) to support styling, generate adaptive icon drawable and generate
-     * bitmap from it.
-     */
-    private Bitmap roundBitmap(Bitmap icon) {
-        final RoundedBitmapDrawable d = RoundedBitmapDrawableFactory.create(getResources(), icon);
-        d.setCornerRadius(R.attr.folderIconRadius);
-        d.setBounds(0, 0, mIconSize, mIconSize);
-        final Bitmap bitmap = Bitmap.createBitmap(d.getBounds().width(), d.getBounds().height(),
-                Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(bitmap);
-        d.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
-        d.draw(canvas);
-        return bitmap;
-    }
-
-    private void setIcon(Bitmap icon, Boolean round) {
-        if (round) {
-            RoundedBitmapDrawable d = RoundedBitmapDrawableFactory.create(getResources(), icon);
-            d.setCornerRadius(R.attr.folderIconRadius);
-            d.setBounds(0, 0, mIconSize, mIconSize);
-            mIconView.setBackground(d);
-        } else {
-            mIconView.setBackground(new BitmapDrawable(getResources(), icon));
-        }
-    }
-
-
-    private Drawable getAppIcon(String pkg) {
-        try {
-            ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(
-                    pkg, 0);
-            return applicationInfo.loadIcon(mPackageManager);
-        } catch (PackageManager.NameNotFoundException ignored) {
-            return null;
-        }
-    }
-
-    private void setupProviderButton(ImageButton button, Bundle provider, Intent intent) {
-        Launcher launcher = Launcher.getLauncher(getContext());
-        button.setOnClickListener(b -> {
-            launcher.startActivitySafely(b, intent, null);
-            Bundle bundle = new Bundle();
-            bundle.putBundle("provider", provider);
-        });
-    }
-
-    @Override
-    public void onClick(View view) {
-        // do nothing.
-    }
-
-    @Override
-    public boolean onLongClick(View view) {
-        // do nothing.
-        return false;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/search/SearchResultThumbnailView.java b/quickstep/src/com/android/launcher3/search/SearchResultThumbnailView.java
deleted file mode 100644
index dd95461..0000000
--- a/quickstep/src/com/android/launcher3/search/SearchResultThumbnailView.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.search;
-
-
-import android.app.search.SearchTarget;
-import android.app.search.SearchTargetEvent;
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.core.graphics.drawable.RoundedBitmapDrawable;
-import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.model.data.SearchActionItemInfo;
-import com.android.launcher3.touch.ItemClickHandler;
-import com.android.launcher3.util.Themes;
-
-import java.util.List;
-
-/**
- * A view representing a high confidence app search result that includes shortcuts
- */
-public class SearchResultThumbnailView extends androidx.appcompat.widget.AppCompatImageView
-        implements SearchTargetHandler {
-
-    private SearchTarget mSearchTarget;
-    private SearchResultIcon mResultIcon;
-
-    public SearchResultThumbnailView(Context context) {
-        super(context);
-    }
-
-    public SearchResultThumbnailView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public SearchResultThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        setOnFocusChangeListener(Launcher.getLauncher(getContext()).getFocusHandler());
-        setOnClickListener(this);
-        setOnLongClickListener(this);
-    }
-
-    @Override
-    public void apply(SearchTarget parentTarget, List<SearchTarget> children) {
-        mSearchTarget = parentTarget;
-        Bitmap bitmap;
-
-        SearchActionItemInfo itemInfo = new SearchActionItemInfo(
-                parentTarget.getSearchAction().getIcon(),
-                parentTarget.getPackageName(),
-                parentTarget.getUserHandle(),
-                parentTarget.getSearchAction().getTitle());
-        itemInfo.setIntent(parentTarget.getSearchAction().getIntent());
-        itemInfo.setPendingIntent(parentTarget.getSearchAction().getPendingIntent());
-
-        bitmap = ((BitmapDrawable) itemInfo.getIcon()
-                .loadDrawable(getContext())).getBitmap();
-        // crop
-        if (bitmap.getWidth() < bitmap.getHeight()) {
-            bitmap = Bitmap.createBitmap(bitmap, 0,
-                    bitmap.getHeight() / 2 - bitmap.getWidth() / 2,
-                    bitmap.getWidth(), bitmap.getWidth());
-        } else {
-            bitmap = Bitmap.createBitmap(bitmap, bitmap.getWidth() / 2 - bitmap.getHeight() / 2,
-                    0,
-                    bitmap.getHeight(), bitmap.getHeight());
-        }
-        setTag(itemInfo);
-
-        RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(null, bitmap);
-        drawable.setCornerRadius(Themes.getDialogCornerRadius(getContext()));
-        setImageDrawable(drawable);
-    }
-
-    @Override
-    public void onClick(View view) {
-        ItemClickHandler.onClickSearchAction(Launcher.getLauncher(getContext()),
-                (SearchActionItemInfo) view.getTag());
-        notifyEvent(getContext(), mSearchTarget.getId(), SearchTargetEvent.ACTION_LAUNCH_TOUCH);
-    }
-
-    @Override
-    public boolean onLongClick(View view) {
-        // do nothing.
-        return false;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/search/SearchResultWidget.java b/quickstep/src/com/android/launcher3/search/SearchResultWidget.java
deleted file mode 100644
index da14959..0000000
--- a/quickstep/src/com/android/launcher3/search/SearchResultWidget.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.search;
-
-import android.app.search.SearchTarget;
-import android.app.search.SearchTargetEvent;
-import android.appwidget.AppWidgetHostView;
-import android.appwidget.AppWidgetProviderInfo;
-import android.content.Context;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.AppWidgetResizeFrame;
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.CheckLongPressHelper;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.allapps.search.SearchWidgetInfoContainer;
-import com.android.launcher3.dragndrop.DraggableView;
-import com.android.launcher3.model.data.PackageItemInfo;
-import com.android.launcher3.touch.ItemLongClickListener;
-import com.android.launcher3.widget.PendingAddWidgetInfo;
-
-import java.util.List;
-
-
-/**
- * displays live version of a widget upon receiving {@link AppWidgetProviderInfo} from Search
- * provider
- */
-public class SearchResultWidget extends LinearLayout implements SearchTargetHandler, DraggableView,
-        View.OnLongClickListener {
-
-
-    private final Rect mWidgetOffset = new Rect();
-
-    private final Launcher mLauncher;
-    private final CheckLongPressHelper mLongPressHelper;
-    private final GestureDetector mClickDetector;
-    private final AppWidgetHostView mHostView;
-    private final float mScaleToFit;
-
-    private SearchWidgetInfoContainer mInfoContainer;
-    private BubbleTextView mWidgetProvider;
-    private TextView mWidgetLabel;
-
-    public SearchResultWidget(@NonNull Context context) {
-        this(context, null, 0);
-    }
-
-    public SearchResultWidget(@NonNull Context context,
-            @Nullable AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public SearchResultWidget(@NonNull Context context, @Nullable AttributeSet attrs,
-            int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        mLauncher = Launcher.getLauncher(context);
-        mHostView = new AppWidgetHostView(context);
-        DeviceProfile grid = mLauncher.getDeviceProfile();
-        mScaleToFit = Math.min(grid.appWidgetScale.x, grid.appWidgetScale.y);
-
-        // detect tap event on widget container for search target event reporting
-        mClickDetector = new GestureDetector(context,
-                new ClickListener(
-                        () -> reportEvent(SearchTargetEvent.ACTION_LAUNCH_TOUCH)));
-        mLongPressHelper = new CheckLongPressHelper(this);
-        mLongPressHelper.setLongPressTimeoutFactor(1);
-        setOnLongClickListener(this);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mWidgetProvider = findViewById(R.id.widget_provider);
-        mWidgetLabel = findViewById(R.id.widget_label);
-        addView(mHostView);
-    }
-
-    @Override
-    public void apply(SearchTarget parentTarget, List<SearchTarget> children) {
-        AppWidgetProviderInfo providerInfo = parentTarget.getAppWidgetProviderInfo();
-        removeListener();
-
-        showWidgetInfo(providerInfo);
-        mInfoContainer = mLauncher.getLiveSearchManager().getPlaceHolderWidget(providerInfo);
-        if (mInfoContainer == null) {
-            setVisibility(GONE);
-            return;
-        }
-        setVisibility(VISIBLE);
-        mInfoContainer.attachWidget(mHostView);
-        PendingAddWidgetInfo info = (PendingAddWidgetInfo) mHostView.getTag();
-        int[] size = mLauncher.getWorkspace().estimateItemSize(info);
-        mHostView.getLayoutParams().width = size[0];
-        mHostView.getLayoutParams().height = size[1];
-        AppWidgetResizeFrame.updateWidgetSizeRanges(mHostView, mLauncher, info.spanX,
-                info.spanY);
-        mHostView.requestLayout();
-        setTag(info);
-    }
-
-    private void showWidgetInfo(AppWidgetProviderInfo providerInfo) {
-        String title = providerInfo.loadLabel(mLauncher.getPackageManager());
-        PackageItemInfo pinfo = new PackageItemInfo(providerInfo.provider.getPackageName());
-        pinfo.user = providerInfo.getProfile();
-        mWidgetProvider.applyFromItemInfoWithIcon(pinfo);
-        mWidgetLabel.setText(title);
-    }
-
-    /**
-     * Stops hostView from getting updates on a widget provider
-     */
-    public void removeListener() {
-        if (mInfoContainer != null) {
-            mInfoContainer.detachWidget(mHostView);
-        }
-    }
-
-    private void reportEvent(int eventType) {
-        SearchSessionTracker.INSTANCE.get(getContext()).notifyEvent(
-                new SearchTargetEvent.Builder("search target id", eventType).build());
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        mLongPressHelper.onTouchEvent(ev);
-        mClickDetector.onTouchEvent(ev);
-        return mLongPressHelper.hasPerformedLongPress();
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent ev) {
-        mLongPressHelper.onTouchEvent(ev);
-        return true;
-    }
-
-    @Override
-    public void cancelLongPress() {
-        super.cancelLongPress();
-        mLongPressHelper.cancelLongPress();
-    }
-
-    @Override
-    public int getViewType() {
-        return DraggableView.DRAGGABLE_WIDGET;
-    }
-
-    @Override
-    public void getSourceVisualDragBounds(Rect bounds) {
-        mHostView.getHitRect(mWidgetOffset);
-        int width = (int) (mHostView.getMeasuredWidth() * mScaleToFit);
-        int height = (int) (mHostView.getMeasuredHeight() * mScaleToFit);
-        bounds.set(mWidgetOffset.left,
-                mWidgetOffset.top,
-                width + mWidgetOffset.left,
-                height + mWidgetOffset.top);
-    }
-
-    @Override
-    public boolean onLongClick(View view) {
-        ItemLongClickListener.INSTANCE_ALL_APPS.onLongClick(view);
-        return false;
-    }
-
-    @Override
-    public void onClick(View view) {
-        // do nothing.
-    }
-
-    static class ClickListener extends GestureDetector.SimpleOnGestureListener {
-        private final Runnable mCb;
-
-        ClickListener(Runnable cb) {
-            mCb = cb;
-        }
-
-        @Override
-        public boolean onSingleTapConfirmed(MotionEvent e) {
-            mCb.run();
-            return super.onSingleTapConfirmed(e);
-        }
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/search/SearchResultWidgetPreview.java b/quickstep/src/com/android/launcher3/search/SearchResultWidgetPreview.java
deleted file mode 100644
index 1786baf..0000000
--- a/quickstep/src/com/android/launcher3/search/SearchResultWidgetPreview.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.search;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-
-import android.app.search.SearchTarget;
-import android.app.search.SearchTargetEvent;
-import android.appwidget.AppWidgetProviderInfo;
-import android.content.Context;
-import android.graphics.Point;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.Toast;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.R;
-import com.android.launcher3.dragndrop.DragOptions;
-import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.touch.ItemLongClickListener;
-import com.android.launcher3.widget.BaseWidgetSheet;
-import com.android.launcher3.widget.PendingItemDragHelper;
-import com.android.launcher3.widget.WidgetCell;
-import com.android.launcher3.widget.WidgetImageView;
-
-import java.util.List;
-
-/**
- * displays preview of a widget upon receiving {@link AppWidgetProviderInfo} from Search provider
- */
-public class SearchResultWidgetPreview extends LinearLayout implements SearchTargetHandler {
-
-    private final Launcher mLauncher;
-    private final LauncherAppState mAppState;
-    private WidgetCell mWidgetCell;
-    private Toast mWidgetToast;
-
-    private String mTargetId;
-    public SearchResultWidgetPreview(Context context) {
-        this(context, null, 0);
-    }
-
-    public SearchResultWidgetPreview(Context context,
-            @Nullable AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public SearchResultWidgetPreview(Context context, @Nullable AttributeSet attrs,
-            int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        mLauncher = Launcher.getLauncher(context);
-        mAppState = LauncherAppState.getInstance(context);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mWidgetCell = findViewById(R.id.widget_cell);
-        mWidgetCell.setOnLongClickListener(this);
-        mWidgetCell.setOnClickListener(this);
-    }
-
-    @Override
-    public void apply(SearchTarget parentTarget, List<SearchTarget> children) {
-        mTargetId = parentTarget.getId();
-        AppWidgetProviderInfo providerInfo = parentTarget.getAppWidgetProviderInfo();
-        LauncherAppWidgetProviderInfo pInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(
-                getContext(), providerInfo);
-        MODEL_EXECUTOR.post(() -> {
-            WidgetItem widgetItem = new WidgetItem(pInfo, mLauncher.getDeviceProfile().inv,
-                    mAppState.getIconCache());
-            MAIN_EXECUTOR.post(() -> {
-                mWidgetCell.applyFromCellItem(widgetItem, mAppState.getWidgetCache());
-                mWidgetCell.ensurePreview();
-            });
-        });
-    }
-
-    @Override
-    public boolean onLongClick(View view) {
-        view.cancelLongPress();
-        if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
-        if (mWidgetCell.getTag() == null) return false;
-
-        WidgetImageView imageView = mWidgetCell.getWidgetView();
-        if (imageView.getBitmap() == null) {
-            return false;
-        }
-
-        int[] loc = new int[2];
-        mLauncher.getDragLayer().getLocationInDragLayer(imageView, loc);
-
-        new PendingItemDragHelper(mWidgetCell).startDrag(
-                imageView.getBitmapBounds(), imageView.getBitmap().getWidth(), imageView.getWidth(),
-                new Point(loc[0], loc[1]), mLauncher.getAppsView(), new DragOptions());
-        reportEvent(SearchTargetEvent.ACTION_LONGPRESS);
-        return true;
-    }
-
-    @Override
-    public void onClick(View view) {
-        mWidgetToast = BaseWidgetSheet.showWidgetToast(getContext(), mWidgetToast);
-        reportEvent(SearchTargetEvent.ACTION_LAUNCH_TOUCH);
-    }
-
-    private void reportEvent(int eventType) {
-        SearchSessionTracker.INSTANCE.get(getContext()).notifyEvent(
-                new SearchTargetEvent.Builder(mTargetId, eventType).build());
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/search/SearchSectionHeaderView.java b/quickstep/src/com/android/launcher3/search/SearchSectionHeaderView.java
deleted file mode 100644
index d7c5c5d..0000000
--- a/quickstep/src/com/android/launcher3/search/SearchSectionHeaderView.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.search;
-
-import android.app.search.SearchTarget;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-import java.util.List;
-
-/**
- * Header text view that shows a title for a given section in All apps search
- */
-public class SearchSectionHeaderView extends TextView implements SearchTargetHandler {
-
-    public SearchSectionHeaderView(Context context) {
-        super(context);
-    }
-
-    public SearchSectionHeaderView(Context context,
-            @Nullable AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public SearchSectionHeaderView(Context context, @Nullable AttributeSet attrs, int styleAttr) {
-        super(context, attrs, styleAttr);
-    }
-
-    @Override
-    public void apply(SearchTarget parentTarget, List<SearchTarget> children) {
-        setText(parentTarget.getSearchAction().getTitle());
-        setVisibility(VISIBLE);
-    }
-
-    @Override
-    public void onClick(View view) {
-        // do nothing.
-    }
-
-    @Override
-    public boolean onLongClick(View view) {
-        // do nothing.
-        return false;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/search/SearchServicePipeline.java b/quickstep/src/com/android/launcher3/search/SearchServicePipeline.java
deleted file mode 100644
index 197aecb..0000000
--- a/quickstep/src/com/android/launcher3/search/SearchServicePipeline.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.search;
-
-import android.app.search.Query;
-import android.app.search.SearchContext;
-import android.app.search.SearchSession;
-import android.app.search.SearchTarget;
-import android.app.search.SearchUiManager;
-import android.content.Context;
-import android.os.CancellationSignal;
-import android.text.TextUtils;
-import android.util.Log;
-
-import androidx.annotation.WorkerThread;
-
-import com.android.app.search.ResultType;
-import com.android.launcher3.allapps.AllAppsGridAdapter;
-import com.android.launcher3.allapps.AllAppsSectionDecorator;
-import com.android.launcher3.allapps.search.SearchPipeline;
-import com.android.launcher3.allapps.search.SearchSectionInfo;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.function.Consumer;
-
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-
-/**
- * Search pipeline utilizing {@link android.app.search.SearchUiManager}
- */
-public class SearchServicePipeline implements SearchPipeline {
-    private static final int SUPPORTED_RESULT_TYPES =
-            ResultType.APPLICATION | ResultType.SHORTCUT | ResultType.PLAY | ResultType.PEOPLE
-                    | ResultType.SETTING;
-
-    private static final boolean DEBUG = true;
-    private static final int REQUEST_TIMEOUT = 200;
-    private static final String TAG = "SearchServicePipeline";
-
-    private final Context mContext;
-    private final SearchSession mSession;
-    private final DeviceSearchAdapterProvider mAdapterProvider;
-
-    private boolean mCanceled = false;
-
-
-    public SearchServicePipeline(Context context, DeviceSearchAdapterProvider adapterProvider) {
-        mContext = context;
-        mAdapterProvider = adapterProvider;
-        SearchUiManager manager = context.getSystemService(SearchUiManager.class);
-        mSession = manager.createSearchSession(
-                new SearchContext(SUPPORTED_RESULT_TYPES, REQUEST_TIMEOUT, null));
-        SearchSessionTracker.getInstance(context).setSearchSession(mSession);
-    }
-
-    @WorkerThread
-    @Override
-    public void query(String input, Consumer<ArrayList<AllAppsGridAdapter.AdapterItem>> callback,
-            CancellationSignal cancellationSignal) {
-        mCanceled = false;
-        Query query = new Query(input, System.currentTimeMillis(), null);
-        mSession.query(query, UI_HELPER_EXECUTOR, targets -> {
-            if (!mCanceled) {
-                if (DEBUG) {
-                    printSearchTargets(input, targets);
-                }
-                SearchSessionTracker.getInstance(mContext).setQuery(query);
-                callback.accept(this.onResult(targets));
-            }
-            Log.w(TAG, "Ignoring results due to cancel signal");
-        });
-    }
-
-    /**
-     * Given A list of search Targets, pairs a group of search targets to a AdapterItem that can
-     * be inflated in AllAppsRecyclerView
-     */
-    private ArrayList<AllAppsGridAdapter.AdapterItem> onResult(List<SearchTarget> searchTargets) {
-        HashMap<String, SearchAdapterItem> adapterMap = new LinkedHashMap<>();
-        List<SearchTarget> unmappedChildren = new ArrayList<>();
-        SearchSectionInfo section = new SearchSectionInfo();
-        section.setDecorationHandler(
-                new AllAppsSectionDecorator.SectionDecorationHandler(mContext, true));
-        for (SearchTarget target : searchTargets) {
-            if (!TextUtils.isEmpty(target.getParentId())) {
-                if (!addChildToParent(target, adapterMap)) {
-                    unmappedChildren.add(target);
-                }
-                continue;
-            }
-            int viewType = mAdapterProvider.getViewTypeForSearchTarget(target);
-            if (viewType != -1) {
-                SearchAdapterItem adapterItem = new SearchAdapterItem(target, viewType);
-                adapterItem.searchSectionInfo = section;
-                adapterMap.put(target.getId(), adapterItem);
-            }
-        }
-        for (SearchTarget s : unmappedChildren) {
-            if (!addChildToParent(s, adapterMap)) {
-                Log.w(TAG,
-                        "Unable to pair child " + s.getId() + " to parent " + s.getParentId());
-            }
-        }
-        return new ArrayList<>(adapterMap.values());
-    }
-
-    private void printSearchTargets(String query, List<SearchTarget> results) {
-        Log.d(TAG, " query=" + query + " size=" + results.size());
-        for (SearchTarget s : results) {
-            Log.d(TAG, "layoutType=" + s.getLayoutType() + " resultType=" + s.getResultType());
-        }
-    }
-
-    /**
-     * Adds a child SearchTarget to a collection of searchTarget children with a shared parentId.
-     * Returns false if no parent searchTarget with id=$parentId does not exists.
-     */
-    private boolean addChildToParent(SearchTarget target, HashMap<String, SearchAdapterItem> map) {
-        if (!map.containsKey(target.getParentId())) return false;
-        map.get(target.getParentId()).getInlineItems().add(target);
-        return true;
-    }
-
-    /**
-     * Unregister callbacks and destroy search session
-     */
-    public void destroy() {
-        mSession.destroy();
-    }
-
-    /**
-     * Cancels current ongoing search request.
-     */
-    public void cancel() {
-        mCanceled = true;
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/search/SearchSessionTracker.java b/quickstep/src/com/android/launcher3/search/SearchSessionTracker.java
deleted file mode 100644
index 3079965..0000000
--- a/quickstep/src/com/android/launcher3/search/SearchSessionTracker.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.search;
-
-import android.app.search.Query;
-import android.app.search.SearchSession;
-import android.app.search.SearchTarget;
-import android.app.search.SearchTargetEvent;
-import android.content.Context;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
-
-import com.android.launcher3.util.MainThreadInitializedObject;
-
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-
-/**
- * A singleton class to track and report search events back to SearchSession
- */
-public class SearchSessionTracker {
-
-    private static final String TAG = "SearchSessionTracker";
-
-    @Nullable
-    private SearchSession mSession;
-    private Query mQuery;
-
-    public static final MainThreadInitializedObject<SearchSessionTracker> INSTANCE =
-            new MainThreadInitializedObject<>(SearchSessionTracker::new);
-
-    private SearchSessionTracker(Context context) {
-    }
-
-    /**
-     * Returns instance of SearchSessionTracker
-     */
-    public static SearchSessionTracker getInstance(Context context) {
-        return SearchSessionTracker.INSTANCE.get(context);
-    }
-
-    @WorkerThread
-    public void setSearchSession(SearchSession session) {
-        mSession = session;
-    }
-
-    @WorkerThread
-    public void setQuery(Query query) {
-        mQuery = query;
-    }
-
-    /**
-     * Send the user event handling back to the {@link SearchSession} object.
-     */
-    public void notifyEvent(SearchTargetEvent event) {
-        if (mSession == null) {
-            Log.d(TAG, "Dropping event " + event.getTargetId());
-        }
-        Log.d(TAG, "notifyEvent:" + mQuery.getInput() + ":" + event.getTargetId());
-        UI_HELPER_EXECUTOR.post(() -> mSession.notifyEvent(mQuery, event));
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/search/SearchTargetHandler.java b/quickstep/src/com/android/launcher3/search/SearchTargetHandler.java
deleted file mode 100644
index 0e7b8f2..0000000
--- a/quickstep/src/com/android/launcher3/search/SearchTargetHandler.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2021 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.search;
-
-import android.app.search.SearchTarget;
-import android.app.search.SearchTargetEvent;
-import android.content.Context;
-import android.view.View;
-
-import java.util.List;
-import java.util.function.Consumer;
-
-/**
- * An interface for supporting dynamic search results
- */
-public interface SearchTargetHandler extends View.OnClickListener, View.OnLongClickListener {
-
-    /**
-     * Update view using values from {@link SearchTarget}
-     */
-    default void apply(SearchTarget parentTarget, List<SearchTarget> children) { }
-
-    /**
-     * Handle IME quick select event. returns whether event was handled.
-     */
-    default boolean quickSelect() {
-        return false;
-    }
-
-    default void notifyEvent(Context context, String id, int eventType) {
-        SearchTargetEvent.Builder builder = new SearchTargetEvent.Builder(id, eventType);
-        SearchSessionTracker.getInstance(context).notifyEvent(builder.build());
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/search/SearchTargetUtil.java b/quickstep/src/com/android/launcher3/search/SearchTargetUtil.java
deleted file mode 100644
index 95f4f58..0000000
--- a/quickstep/src/com/android/launcher3/search/SearchTargetUtil.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright (C) 2021 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.search;
-
-import android.app.PendingIntent;
-import android.app.search.SearchAction;
-import android.app.search.SearchTarget;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ShortcutInfo;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.drawable.Icon;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Process;
-import android.os.UserHandle;
-
-import com.android.app.search.ResultType;
-
-import static com.android.app.search.LayoutType.ICON_DOUBLE_HORIZONTAL_TEXT;
-import static com.android.app.search.LayoutType.ICON_SINGLE_HORIZONTAL_TEXT;
-import static com.android.app.search.LayoutType.THUMBNAIL;
-import static com.android.app.search.ResultType.ACTION;
-import static com.android.app.search.ResultType.SCREENSHOT;
-import static com.android.app.search.ResultType.SUGGEST;
-
-public class SearchTargetUtil {
-
-    public static final String BUNDLE_EXTRA_SHOULD_START = "should_start";
-    public static final String BUNDLE_EXTRA_SHOULD_START_FOR_RESULT = "should_start_for_result";
-    public static final String BUNDLE_EXTRA_BADGE_FROM_ICON = "badge_from_icon";
-    public static final String BUNDLE_EXTRA_PRIMARY_ICON_FROM_TITLE = "primary_icon_from_title";
-
-    public static final String EXTRA_CLASS = "class";
-
-    private static final String TITLE = " title: weather ";
-    private static final String SUBTITLE = " subtitle: 68 degrees ";
-    private static final String PACKAGE2 = "com.google.android.gm";
-    private static final UserHandle USERHANDLE = Process.myUserHandle();
-
-
-    /**
-     * Generate SearchTargetUtil for ICON_DOUBLE_HORIZONTAL_TEXT layout type.
-     *
-       targets.add(SearchTargetUtil.generateIconDoubleHorizontalText_SearchAction(
-           mContext, "red", Color.RED));
-       targets.add(SearchTargetUtil.generateIconDoubleHorizontalText_SearchAction(
-           mContext, "yellow", Color.YELLOW));
-     */
-    public static SearchTarget generateIconDoubleHorizontalText_SearchAction(
-            Context context, String id, int color) {
-        SearchTarget.Builder builder =
-                new SearchTarget.Builder(ACTION, ICON_DOUBLE_HORIZONTAL_TEXT, id)
-                        .setPackageName(PACKAGE2) /* required */
-                        .setUserHandle(USERHANDLE); /* required */
-
-        Intent intent = new Intent("com.google.android.googlequicksearchbox.GENERIC_QUERY");
-        intent.putExtra("query", "weather");
-        intent.putExtra("full_screen", false);
-        PendingIntent pendingIntent =
-                PendingIntent.getActivity(
-                        context, 1, intent,
-                        PendingIntent.FLAG_UPDATE_CURRENT);
-
-        Bitmap bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(bitmap);
-        canvas.drawColor(color);
-        Icon icon = Icon.createWithAdaptiveBitmap(bitmap);
-
-        Bundle b = new Bundle();
-        b.putBoolean(BUNDLE_EXTRA_SHOULD_START_FOR_RESULT, true);
-        b.putBoolean(BUNDLE_EXTRA_BADGE_FROM_ICON, true);
-        b.putBoolean(BUNDLE_EXTRA_PRIMARY_ICON_FROM_TITLE, true);
-
-        builder.setSearchAction(new SearchAction.Builder(id, id + TITLE)
-                .setSubtitle(id + SUBTITLE)
-                .setPendingIntent(pendingIntent)
-                .setIcon(icon)
-                .setExtras(b)
-                .build());
-        return builder.build();
-    }
-
-    /**
-     *  Inside SearchServicePipeline, add following samples to test the search target.
-     *
-     * targets.add(SearchTargetUtil.generateThumbnail_SearchAction("blue", Color.BLUE));
-     * targets.add(SearchTargetUtil.generateThumbnail_SearchAction("red", Color.RED));
-     * targets.add(SearchTargetUtil.generateThumbnail_SearchAction("green", Color.GREEN));
-     */
-    public static SearchTarget generateThumbnail_SearchAction(String id, int color) {
-        SearchTarget.Builder builder =
-                new SearchTarget.Builder(SCREENSHOT, THUMBNAIL, id)
-                        .setPackageName(PACKAGE2) /* required */
-                        .setUserHandle(USERHANDLE); /* required */
-
-
-        Intent intent = new Intent(Intent.ACTION_VIEW)
-                .setData(Uri.parse("uri blah blah"))
-                .setType("image/*")
-                .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
-                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
-        Bitmap bitmap = Bitmap.createBitmap(1000, 500, Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(bitmap);
-        canvas.drawColor(color);
-        Icon icon = Icon.createWithBitmap(bitmap);
-
-        builder.setSearchAction(new SearchAction.Builder(id, TITLE)
-                .setSubtitle(SUBTITLE)
-                .setIcon(icon)
-                .setIntent(intent)
-                .build());
-        return builder.build();
-
-    }
-
-    /**
-     *
-     * targets.add(SearchTargetUtil.generateIconHorizontalText_SearchAction(
-     *     mContext, "red", Color.RED));
-     * targets.add(SearchTargetUtil.generateIconHorizontalText_SearchAction(
-     *     mContext, "yellow", Color.YELLOW));
-     */
-    public static SearchTarget generateIconHorizontalText_SearchAction(
-            Context context, String id, int color) {
-        String fallbackQuery = "How to make cookie";
-        SearchTarget.Builder builder =
-                new SearchTarget.Builder(SUGGEST, ICON_SINGLE_HORIZONTAL_TEXT, id)
-                        .setPackageName(PACKAGE2) /* required */
-                        .setUserHandle(USERHANDLE); /* required */
-
-        Intent intent3 = new Intent("com.google.android.googlequicksearchbox.GENERIC_QUERY");
-        intent3.putExtra("query", fallbackQuery);
-        intent3.putExtra("full_screen", false);
-        PendingIntent pendingIntent3 =
-                PendingIntent.getActivity(
-                        context, 1, intent3,
-                        PendingIntent.FLAG_UPDATE_CURRENT);
-
-        Bitmap bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(bitmap);
-        canvas.drawColor(color);
-        Icon icon = Icon.createWithAdaptiveBitmap(bitmap);
-
-        Bundle extra = new Bundle();
-        extra.putBoolean(BUNDLE_EXTRA_SHOULD_START_FOR_RESULT, true);
-
-        SearchAction searchAction = new SearchAction.Builder(id, fallbackQuery)
-                .setPendingIntent(pendingIntent3)
-                .setIcon(icon)
-                .setExtras(extra)
-                .build();
-        return builder.setSearchAction(searchAction).build();
-    }
-
-
-    /**
-     * Generate SearchTargetUtil for ICON_DOUBLE_HORIZONTAL_TEXT layout type.
-     */
-    public static SearchTarget generateIconDoubleHorizontalText_ShortcutInfo(Context context) {
-        String id = "23456";
-        SearchTarget.Builder builder =
-                new SearchTarget.Builder(ResultType.SHORTCUT, ICON_DOUBLE_HORIZONTAL_TEXT, id)
-                        .setPackageName("com.google.android.gm") /* required */
-                        .setUserHandle(UserHandle.CURRENT); /* required */
-
-        builder.setShortcutInfo(new ShortcutInfo.Builder(context, id).build());
-        return builder.build();
-    }
-}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
new file mode 100644
index 0000000..8312b82
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2021 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.taskbar;
+
+import android.content.ContextWrapper;
+import android.graphics.Rect;
+import android.view.LayoutInflater;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.BaseDragLayer;
+
+/**
+ * The {@link ActivityContext} with which we inflate Taskbar-related Views. This allows UI elements
+ * that are used by both Launcher and Taskbar (such as Folder) to reference a generic
+ * ActivityContext and BaseDragLayer instead of the Launcher activity and its DragLayer.
+ */
+public class TaskbarActivityContext extends ContextWrapper implements ActivityContext {
+
+    private final DeviceProfile mDeviceProfile;
+    private final LayoutInflater mLayoutInflater;
+    private final TaskbarContainerView mTaskbarContainerView;
+    private final float mIconScale;
+
+    public TaskbarActivityContext(BaseQuickstepLauncher launcher) {
+        super(launcher);
+        mDeviceProfile = launcher.getDeviceProfile().copy(this);
+        float taskbarIconSize = getResources().getDimension(R.dimen.taskbar_icon_size);
+        mIconScale = taskbarIconSize / mDeviceProfile.iconSizePx;
+        mDeviceProfile.updateIconSize(mIconScale, getResources());
+
+        mLayoutInflater = LayoutInflater.from(this).cloneInContext(this);
+
+        mTaskbarContainerView = (TaskbarContainerView) mLayoutInflater
+                .inflate(R.layout.taskbar, null, false);
+    }
+
+    public TaskbarContainerView getTaskbarContainerView() {
+        return mTaskbarContainerView;
+    }
+
+    /**
+     * @return A LayoutInflater to use in this Context. Views inflated with this LayoutInflater will
+     * be able to access this TaskbarActivityContext via ActivityContext.lookupContext().
+     */
+    public LayoutInflater getLayoutInflater() {
+        return mLayoutInflater;
+    }
+
+    @Override
+    public BaseDragLayer<TaskbarActivityContext> getDragLayer() {
+        return mTaskbarContainerView;
+    }
+
+    @Override
+    public DeviceProfile getDeviceProfile() {
+        return mDeviceProfile;
+    }
+
+    @Override
+    public Rect getFolderBoundingBox() {
+        return mTaskbarContainerView.getFolderBoundingBox();
+    }
+
+    /**
+     * @return The ratio of taskbar icon size vs normal workspace/hotseat icon size.
+     */
+    public float getTaskbarIconScale() {
+        return mIconScale;
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java
index 0093e66..1e5e3e7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java
@@ -15,17 +15,38 @@
  */
 package com.android.launcher3.taskbar;
 
+import static com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo.TOUCHABLE_INSETS_FRAME;
+import static com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo.TOUCHABLE_INSETS_REGION;
+
 import android.content.Context;
+import android.graphics.Rect;
 import android.util.AttributeSet;
-import android.widget.FrameLayout;
+import android.view.View;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AlphaUpdateListener;
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.views.BaseDragLayer;
+import com.android.systemui.shared.system.ViewTreeObserverWrapper;
+
 /**
  * Top-level ViewGroup that hosts the TaskbarView as well as Views created by it such as Folder.
  */
-public class TaskbarContainerView extends FrameLayout {
+public class TaskbarContainerView extends BaseDragLayer<TaskbarActivityContext> {
+
+    private final int[] mTempLoc = new int[2];
+    private final int mFolderMargin;
+
+    // Initialized in TaskbarController constructor.
+    private TaskbarController.TaskbarContainerViewCallbacks mControllerCallbacks;
+
+    // Initialized in init.
+    private TaskbarView mTaskbarView;
+    private ViewTreeObserverWrapper.OnComputeInsetsListener mTaskbarInsetsComputer;
+
     public TaskbarContainerView(@NonNull Context context) {
         this(context, null);
     }
@@ -41,6 +62,96 @@
 
     public TaskbarContainerView(@NonNull Context context, @Nullable AttributeSet attrs,
             int defStyleAttr, int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
+        super(context, attrs, 1 /* alphaChannelCount */);
+        mFolderMargin = getResources().getDimensionPixelSize(R.dimen.taskbar_folder_margin);
+    }
+
+    protected void construct(TaskbarController.TaskbarContainerViewCallbacks callbacks) {
+        mControllerCallbacks = callbacks;
+    }
+
+    protected void init(TaskbarView taskbarView) {
+        mTaskbarView = taskbarView;
+        mTaskbarInsetsComputer = createTaskbarInsetsComputer();
+        recreateControllers();
+    }
+
+    @Override
+    public void recreateControllers() {
+        mControllers = new TouchController[0];
+    }
+
+    private ViewTreeObserverWrapper.OnComputeInsetsListener createTaskbarInsetsComputer() {
+        return insetsInfo -> {
+            if (getAlpha() < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) {
+                // We're invisible, let touches pass through us.
+                insetsInfo.touchableRegion.setEmpty();
+                insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION);
+            } else {
+                 // We're visible again, accept touches anywhere in our bounds.
+                insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME);
+            }
+
+            // TaskbarContainerView provides insets to other apps based on contentInsets. These
+            // insets should stay consistent even if we expand TaskbarContainerView's bounds, e.g.
+            // to show a floating view like Folder. Thus, we set the contentInsets to be where
+            // mTaskbarView is, since its position never changes and insets rather than overlays.
+            int[] loc = mTempLoc;
+            float scale = mTaskbarView.getScaleX();
+            mTaskbarView.setScaleX(1);
+            mTaskbarView.setScaleY(1);
+            mTaskbarView.getLocationInWindow(loc);
+            mTaskbarView.setScaleX(scale);
+            mTaskbarView.setScaleY(scale);
+            insetsInfo.contentInsets.left = loc[0];
+            insetsInfo.contentInsets.top = loc[1];
+            insetsInfo.contentInsets.right = getWidth() - (loc[0] + mTaskbarView.getWidth());
+            insetsInfo.contentInsets.bottom = getHeight() - (loc[1] + mTaskbarView.getHeight());
+        };
+    }
+
+    protected void cleanup() {
+        ViewTreeObserverWrapper.removeOnComputeInsetsListener(mTaskbarInsetsComputer);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        ViewTreeObserverWrapper.addOnComputeInsetsListener(getViewTreeObserver(),
+                mTaskbarInsetsComputer);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        cleanup();
+    }
+
+    @Override
+    protected boolean canFindActiveController() {
+        // Unlike super class, we want to be able to find controllers when touches occur in the
+        // gesture area. For example, this allows Folder to close itself when touching the Taskbar.
+        return true;
+    }
+
+    @Override
+    public void onViewRemoved(View child) {
+        super.onViewRemoved(child);
+        mControllerCallbacks.onViewRemoved();
+    }
+
+    /**
+     * @return Bounds (in our coordinates) where an opened Folder can display.
+     */
+    protected Rect getFolderBoundingBox() {
+        Rect boundingBox = new Rect(0, 0, getWidth(), getHeight() - mTaskbarView.getHeight());
+        boundingBox.inset(mFolderMargin, mFolderMargin);
+        return boundingBox;
+    }
+
+    protected TaskbarActivityContext getTaskbarActivityContext() {
+        return mActivity;
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
index 7be1b92..544835c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
@@ -19,18 +19,45 @@
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_BOTTOM_TAPPABLE_ELEMENT;
 import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_EXTRA_NAVIGATION_BAR;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.ActivityOptions;
+import android.content.ComponentName;
 import android.graphics.PixelFormat;
 import android.graphics.Point;
+import android.graphics.Rect;
 import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
 import android.view.WindowManager;
 
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.QuickstepAppTransitionManagerImpl;
 import com.android.launcher3.R;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.folder.Folder;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.touch.ItemClickHandler;
+import com.android.quickstep.AnimatedFloat;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Interfaces with Launcher/WindowManager/SystemUI to determine what to show in TaskbarView.
  */
@@ -44,42 +71,199 @@
     private final WindowManager mWindowManager;
     // Layout width and height of the Taskbar in the default state.
     private final Point mTaskbarSize;
+    private final TaskbarStateHandler mTaskbarStateHandler;
+    private final TaskbarVisibilityController mTaskbarVisibilityController;
+    private final TaskbarHotseatController mHotseatController;
+    private final TaskbarRecentsController mRecentsController;
+    private final TaskbarDragController mDragController;
 
+    // Initialized in init().
     private WindowManager.LayoutParams mWindowLayoutParams;
 
+    // Contains all loaded Tasks, not yet deduped from Hotseat items.
+    private List<Task> mLatestLoadedRecentTasks;
+    // Contains all loaded Hotseat items.
+    private ItemInfo[] mLatestLoadedHotseatItems;
+
+    private @Nullable Animator mAnimator;
+    private boolean mIsAnimatingToLauncher;
+
     public TaskbarController(BaseQuickstepLauncher launcher,
             TaskbarContainerView taskbarContainerView) {
         mLauncher = launcher;
         mTaskbarContainerView = taskbarContainerView;
+        mTaskbarContainerView.construct(createTaskbarContainerViewCallbacks());
         mTaskbarView = mTaskbarContainerView.findViewById(R.id.taskbar_view);
+        mTaskbarView.construct(createTaskbarViewCallbacks());
         mWindowManager = mLauncher.getWindowManager();
-        mTaskbarSize = new Point(MATCH_PARENT,
-                mLauncher.getResources().getDimensionPixelSize(R.dimen.taskbar_size));
+        mTaskbarSize = new Point(MATCH_PARENT, mLauncher.getDeviceProfile().taskbarSize);
+        mTaskbarStateHandler = mLauncher.getTaskbarStateHandler();
+        mTaskbarVisibilityController = new TaskbarVisibilityController(mLauncher,
+                createTaskbarVisibilityControllerCallbacks());
+        mHotseatController = new TaskbarHotseatController(mLauncher,
+                createTaskbarHotseatControllerCallbacks());
+        mRecentsController = new TaskbarRecentsController(mLauncher,
+                createTaskbarRecentsControllerCallbacks());
+        mDragController = new TaskbarDragController(mLauncher);
+    }
+
+    private TaskbarVisibilityControllerCallbacks createTaskbarVisibilityControllerCallbacks() {
+        return new TaskbarVisibilityControllerCallbacks() {
+            @Override
+            public void updateTaskbarBackgroundAlpha(float alpha) {
+                mTaskbarView.setBackgroundAlpha(alpha);
+            }
+
+            @Override
+            public void updateTaskbarVisibilityAlpha(float alpha) {
+                mTaskbarContainerView.setAlpha(alpha);
+            }
+        };
+    }
+
+    private TaskbarContainerViewCallbacks createTaskbarContainerViewCallbacks() {
+        return new TaskbarContainerViewCallbacks() {
+            @Override
+            public void onViewRemoved() {
+                if (mTaskbarContainerView.getChildCount() == 1) {
+                    // Only TaskbarView remains.
+                    setTaskbarWindowFullscreen(false);
+                }
+            }
+        };
+    }
+
+    private TaskbarViewCallbacks createTaskbarViewCallbacks() {
+        return new TaskbarViewCallbacks() {
+            @Override
+            public View.OnClickListener getItemOnClickListener() {
+                return view -> {
+                    Object tag = view.getTag();
+                    if (tag instanceof Task) {
+                        Task task = (Task) tag;
+                        ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key,
+                                ActivityOptions.makeBasic());
+                    } else if (tag instanceof FolderInfo) {
+                        FolderIcon folderIcon = (FolderIcon) view;
+                        Folder folder = folderIcon.getFolder();
+
+                        setTaskbarWindowFullscreen(true);
+
+                        mTaskbarContainerView.post(() -> {
+                            folder.animateOpen();
+
+                            folder.iterateOverItems((itemInfo, itemView) -> {
+                                itemView.setOnClickListener(getItemOnClickListener());
+                                itemView.setOnLongClickListener(getItemOnLongClickListener());
+                                // To play haptic when dragging, like other Taskbar items do.
+                                itemView.setHapticFeedbackEnabled(true);
+                                return false;
+                            });
+                        });
+                    } else {
+                        ItemClickHandler.INSTANCE.onClick(view);
+                    }
+
+                    AbstractFloatingView.closeAllOpenViews(
+                            mTaskbarContainerView.getTaskbarActivityContext());
+                };
+            }
+
+            @Override
+            public View.OnLongClickListener getItemOnLongClickListener() {
+                return view -> {
+                    if (mLauncher.hasBeenResumed() && view.getTag() instanceof ItemInfo) {
+                        alignRealHotseatWithTaskbar();
+                        return mDragController.startWorkspaceDragOnLongClick(view);
+                    } else {
+                        return mDragController.startSystemDragOnLongClick(view);
+                    }
+                };
+            }
+
+            @Override
+            public int getEmptyHotseatViewVisibility() {
+                // When on the home screen, we want the empty hotseat views to take up their full
+                // space so that the others line up with the home screen hotseat.
+                return mLauncher.hasBeenResumed() || mIsAnimatingToLauncher
+                        ? View.INVISIBLE : View.GONE;
+            }
+        };
+    }
+
+    private TaskbarHotseatControllerCallbacks createTaskbarHotseatControllerCallbacks() {
+        return new TaskbarHotseatControllerCallbacks() {
+            @Override
+            public void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
+                mTaskbarView.updateHotseatItems(hotseatItemInfos);
+                mLatestLoadedHotseatItems = hotseatItemInfos;
+                dedupeAndUpdateRecentItems();
+            }
+        };
+    }
+
+    private TaskbarRecentsControllerCallbacks createTaskbarRecentsControllerCallbacks() {
+        return new TaskbarRecentsControllerCallbacks() {
+            @Override
+            public void updateRecentItems(ArrayList<Task> recentTasks) {
+                mLatestLoadedRecentTasks = recentTasks;
+                dedupeAndUpdateRecentItems();
+            }
+
+            @Override
+            public void updateRecentTaskAtIndex(int taskIndex, Task task) {
+                mTaskbarView.updateRecentTaskAtIndex(taskIndex, task);
+            }
+        };
     }
 
     /**
      * Initializes the Taskbar, including adding it to the screen.
      */
     public void init() {
+        mTaskbarView.init(mHotseatController.getNumHotseatIcons(),
+                mRecentsController.getNumRecentIcons());
+        mTaskbarContainerView.init(mTaskbarView);
         addToWindowManager();
+        mTaskbarStateHandler.setTaskbarCallbacks(createTaskbarStateHandlerCallbacks());
+        mTaskbarVisibilityController.init();
+        mHotseatController.init();
+        mRecentsController.init();
+
+        SCALE_PROPERTY.set(mTaskbarView, mLauncher.hasBeenResumed() ? getTaskbarScaleOnHome() : 1f);
+    }
+
+    private TaskbarStateHandlerCallbacks createTaskbarStateHandlerCallbacks() {
+        return new TaskbarStateHandlerCallbacks() {
+            @Override
+            public AnimatedFloat getAlphaTarget() {
+                return mTaskbarVisibilityController.getTaskbarVisibilityForLauncherState();
+            }
+        };
     }
 
     /**
      * Removes the Taskbar from the screen, and removes any obsolete listeners etc.
      */
     public void cleanup() {
+        mTaskbarView.cleanup();
+        mTaskbarContainerView.cleanup();
         removeFromWindowManager();
-    }
+        mTaskbarStateHandler.setTaskbarCallbacks(null);
+        mTaskbarVisibilityController.cleanup();
+        mHotseatController.cleanup();
+        mRecentsController.cleanup();
 
-    private void removeFromWindowManager() {
-        if (mTaskbarContainerView.isAttachedToWindow()) {
-            mWindowManager.removeViewImmediate(mTaskbarContainerView);
+        if (mAnimator != null) {
+            mAnimator.end();
         }
     }
 
-    private void addToWindowManager() {
-        removeFromWindowManager();
+    private void removeFromWindowManager() {
+        mWindowManager.removeViewImmediate(mTaskbarContainerView);
+    }
 
+    private void addToWindowManager() {
         final int gravity = Gravity.BOTTOM;
 
         mWindowLayoutParams = new WindowManager.LayoutParams(
@@ -94,6 +278,7 @@
         mWindowLayoutParams.setFitInsetsTypes(0);
         mWindowLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
         mWindowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+        mWindowLayoutParams.setSystemApplicationOverlay(true);
 
         WindowManagerWrapper wmWrapper = WindowManagerWrapper.getInstance();
         wmWrapper.setProvidesInsetsTypes(
@@ -108,4 +293,226 @@
 
         mWindowManager.addView(mTaskbarContainerView, mWindowLayoutParams);
     }
+
+    /**
+     * Should be called from onResume() and onPause(), and animates the Taskbar accordingly.
+     */
+    public void onLauncherResumedOrPaused(boolean isResumed) {
+        long duration = QuickstepAppTransitionManagerImpl.CONTENT_ALPHA_DURATION;
+        if (mAnimator != null) {
+            mAnimator.cancel();
+        }
+        if (isResumed) {
+            mAnimator = createAnimToLauncher(null, duration);
+        } else {
+            mAnimator = createAnimToApp(duration);
+        }
+        mAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mAnimator = null;
+            }
+        });
+        mAnimator.start();
+    }
+
+    /**
+     * Create Taskbar animation when going from an app to Launcher.
+     * @param toState If known, the state we will end up in when reaching Launcher.
+     */
+    public Animator createAnimToLauncher(@Nullable LauncherState toState, long duration) {
+        PendingAnimation anim = new PendingAnimation(duration);
+        anim.add(mTaskbarVisibilityController.createAnimToBackgroundAlpha(0, duration));
+        if (toState != null) {
+            mTaskbarStateHandler.setStateWithAnimation(toState, new StateAnimationConfig(), anim);
+        }
+        anim.addFloat(mTaskbarView, SCALE_PROPERTY, mTaskbarView.getScaleX(),
+                getTaskbarScaleOnHome(), LINEAR);
+
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mIsAnimatingToLauncher = true;
+                mTaskbarView.updateHotseatItemsVisibility();
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mIsAnimatingToLauncher = false;
+            }
+        });
+
+        anim.addOnFrameCallback(this::alignRealHotseatWithTaskbar);
+
+        return anim.buildAnim();
+    }
+
+    private Animator createAnimToApp(long duration) {
+        PendingAnimation anim = new PendingAnimation(duration);
+        anim.add(mTaskbarVisibilityController.createAnimToBackgroundAlpha(1, duration));
+        anim.addFloat(mTaskbarView, SCALE_PROPERTY, mTaskbarView.getScaleX(), 1f, LINEAR);
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mTaskbarView.updateHotseatItemsVisibility();
+            }
+        });
+        return anim.buildAnim();
+    }
+
+    /**
+     * Should be called when the IME visibility changes, so we can hide/show Taskbar accordingly.
+     */
+    public void setIsImeVisible(boolean isImeVisible) {
+        mTaskbarVisibilityController.animateToVisibilityForIme(isImeVisible ? 0 : 1);
+    }
+
+    /**
+     * Should be called when one or more items in the Hotseat have changed.
+     */
+    public void onHotseatUpdated() {
+        mHotseatController.onHotseatUpdated();
+    }
+
+    /**
+     * @param ev MotionEvent in screen coordinates.
+     * @return Whether any Taskbar item could handle the given MotionEvent if given the chance.
+     */
+    public boolean isEventOverAnyTaskbarItem(MotionEvent ev) {
+        return mTaskbarView.isEventOverAnyItem(ev);
+    }
+
+    public boolean isDraggingItem() {
+        return mTaskbarView.isDraggingItem();
+    }
+
+    private void dedupeAndUpdateRecentItems() {
+        if (mLatestLoadedRecentTasks == null || mLatestLoadedHotseatItems == null) {
+            return;
+        }
+
+        final int numRecentIcons = mRecentsController.getNumRecentIcons();
+
+        // From most recent to least recently opened.
+        List<Task> dedupedTasksInDescendingOrder = new ArrayList<>();
+        for (int i = mLatestLoadedRecentTasks.size() - 1; i >= 0; i--) {
+            Task task = mLatestLoadedRecentTasks.get(i);
+            boolean isTaskInHotseat = false;
+            for (ItemInfo hotseatItem : mLatestLoadedHotseatItems) {
+                if (hotseatItem == null) {
+                    continue;
+                }
+                ComponentName hotseatActivity = hotseatItem.getTargetComponent();
+                if (hotseatActivity != null && task.key.sourceComponent.getPackageName()
+                        .equals(hotseatActivity.getPackageName())) {
+                    isTaskInHotseat = true;
+                    break;
+                }
+            }
+            if (!isTaskInHotseat) {
+                dedupedTasksInDescendingOrder.add(task);
+                if (dedupedTasksInDescendingOrder.size() == numRecentIcons) {
+                    break;
+                }
+            }
+        }
+
+        // TaskbarView expects an array of all the recent tasks to show, in the order to show them.
+        // So we create an array of the proper size, then fill it in such that the most recent items
+        // are at the end. If there aren't enough elements to fill the array, leave them null.
+        Task[] tasksArray = new Task[numRecentIcons];
+        for (int i = 0; i < tasksArray.length; i++) {
+            Task task = i >= dedupedTasksInDescendingOrder.size()
+                    ? null
+                    : dedupedTasksInDescendingOrder.get(i);
+            tasksArray[tasksArray.length - 1 - i] = task;
+        }
+
+        mTaskbarView.updateRecentTasks(tasksArray);
+        mRecentsController.loadIconsForTasks(tasksArray);
+    }
+
+    /**
+     * @return Whether the given View is in the same window as Taskbar.
+     */
+    public boolean isViewInTaskbar(View v) {
+        return mTaskbarContainerView.isAttachedToWindow()
+                && mTaskbarContainerView.getWindowId().equals(v.getWindowId());
+    }
+
+    /**
+     * Pads the Hotseat to line up exactly with Taskbar's copy of the Hotseat.
+     */
+    public void alignRealHotseatWithTaskbar() {
+        Rect hotseatBounds = new Rect();
+        mTaskbarView.getHotseatBoundsAtScale(getTaskbarScaleOnHome()).roundOut(hotseatBounds);
+        mLauncher.getHotseat().setPadding(hotseatBounds.left, hotseatBounds.top,
+                mTaskbarView.getWidth() - hotseatBounds.right,
+                mTaskbarView.getHeight() - hotseatBounds.bottom);
+    }
+
+    private float getTaskbarScaleOnHome() {
+        return 1f / mTaskbarContainerView.getTaskbarActivityContext().getTaskbarIconScale();
+    }
+
+    /**
+     * Updates the TaskbarContainer to MATCH_PARENT vs original Taskbar size.
+     */
+    private void setTaskbarWindowFullscreen(boolean fullscreen) {
+        if (fullscreen) {
+            mWindowLayoutParams.width = MATCH_PARENT;
+            mWindowLayoutParams.height = MATCH_PARENT;
+        } else {
+            mWindowLayoutParams.width = mTaskbarSize.x;
+            mWindowLayoutParams.height = mTaskbarSize.y;
+        }
+        mWindowManager.updateViewLayout(mTaskbarContainerView, mWindowLayoutParams);
+    }
+
+    /**
+     * Contains methods that TaskbarStateHandler can call to interface with TaskbarController.
+     */
+    protected interface TaskbarStateHandlerCallbacks {
+        AnimatedFloat getAlphaTarget();
+    }
+
+    /**
+     * Contains methods that TaskbarVisibilityController can call to interface with
+     * TaskbarController.
+     */
+    protected interface TaskbarVisibilityControllerCallbacks {
+        void updateTaskbarBackgroundAlpha(float alpha);
+        void updateTaskbarVisibilityAlpha(float alpha);
+    }
+
+    /**
+     * Contains methods that TaskbarContainerView can call to interface with TaskbarController.
+     */
+    protected interface TaskbarContainerViewCallbacks {
+        void onViewRemoved();
+    }
+
+    /**
+     * Contains methods that TaskbarView can call to interface with TaskbarController.
+     */
+    protected interface TaskbarViewCallbacks {
+        View.OnClickListener getItemOnClickListener();
+        View.OnLongClickListener getItemOnLongClickListener();
+        int getEmptyHotseatViewVisibility();
+    }
+
+    /**
+     * Contains methods that TaskbarHotseatController can call to interface with TaskbarController.
+     */
+    protected interface TaskbarHotseatControllerCallbacks {
+        void updateHotseatItems(ItemInfo[] hotseatItemInfos);
+    }
+
+    /**
+     * Contains methods that TaskbarRecentsController can call to interface with TaskbarController.
+     */
+    protected interface TaskbarRecentsControllerCallbacks {
+        void updateRecentItems(ArrayList<Task> recentTasks);
+        void updateRecentTaskAtIndex(int taskIndex, Task task);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
new file mode 100644
index 0000000..f51e498
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2021 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.taskbar;
+
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.os.UserHandle;
+import android.view.DragEvent;
+import android.view.View;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.ClipDescriptionCompat;
+import com.android.systemui.shared.system.LauncherAppsCompat;
+
+/**
+ * Handles long click on Taskbar items to start a system drag and drop operation.
+ */
+public class TaskbarDragController {
+
+    private final BaseQuickstepLauncher mLauncher;
+    private final int mDragIconSize;
+
+    public TaskbarDragController(BaseQuickstepLauncher launcher) {
+        mLauncher = launcher;
+        Resources resources = mLauncher.getResources();
+        mDragIconSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_drag_icon_size);
+    }
+
+    /**
+     * Attempts to start a system drag and drop operation for the given View, using its tag to
+     * generate the ClipDescription and Intent.
+     * @return Whether {@link View#startDragAndDrop} started successfully.
+     */
+    protected boolean startSystemDragOnLongClick(View view) {
+        if (!(view instanceof BubbleTextView)) {
+            return false;
+        }
+
+        BubbleTextView btv = (BubbleTextView) view;
+
+        View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view) {
+            @Override
+            public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
+                shadowSize.set(mDragIconSize, mDragIconSize);
+                // TODO: should be based on last touch point on the icon.
+                shadowTouchPoint.set(shadowSize.x / 2, shadowSize.y / 2);
+            }
+
+            @Override
+            public void onDrawShadow(Canvas canvas) {
+                canvas.save();
+                float scale = (float) mDragIconSize / btv.getIconSize();
+                canvas.scale(scale, scale);
+                btv.getIcon().draw(canvas);
+                canvas.restore();
+            }
+        };
+
+        Object tag = view.getTag();
+        ClipDescription clipDescription = null;
+        Intent intent = null;
+        if (tag instanceof WorkspaceItemInfo) {
+            WorkspaceItemInfo item = (WorkspaceItemInfo) tag;
+            LauncherApps launcherApps = mLauncher.getSystemService(LauncherApps.class);
+            clipDescription = new ClipDescription(item.title,
+                    new String[] {
+                            item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+                                    ? ClipDescriptionCompat.MIMETYPE_APPLICATION_SHORTCUT
+                                    : ClipDescriptionCompat.MIMETYPE_APPLICATION_ACTIVITY
+                    });
+            intent = new Intent();
+            if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                intent.putExtra(Intent.EXTRA_PACKAGE_NAME, item.getIntent().getPackage());
+                intent.putExtra(Intent.EXTRA_SHORTCUT_ID, item.getDeepShortcutId());
+            } else {
+                intent.putExtra(ClipDescriptionCompat.EXTRA_PENDING_INTENT,
+                        LauncherAppsCompat.getMainActivityLaunchIntent(launcherApps,
+                                item.getIntent().getComponent(), null, item.user));
+            }
+            intent.putExtra(Intent.EXTRA_USER, item.user);
+        } else if (tag instanceof Task) {
+            Task task = (Task) tag;
+            clipDescription = new ClipDescription(task.titleDescription,
+                    new String[] {
+                            ClipDescriptionCompat.MIMETYPE_APPLICATION_TASK
+                    });
+            intent = new Intent();
+            intent.putExtra(ClipDescriptionCompat.EXTRA_TASK_ID, task.key.id);
+            intent.putExtra(Intent.EXTRA_USER, UserHandle.of(task.key.userId));
+        }
+
+        if (clipDescription != null && intent != null) {
+            ClipData clipData = new ClipData(clipDescription, new ClipData.Item(intent));
+            view.setOnDragListener(getDraggedViewDragListener());
+            return view.startDragAndDrop(clipData, shadowBuilder, null /* localState */,
+                    View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_OPAQUE);
+        }
+        return false;
+    }
+
+    /**
+     * Starts a drag and drop operation that controls a real Workspace (Hotseat) view.
+     * @param view The Taskbar item that was long clicked.
+     * @return Whether {@link View#startDragAndDrop} started successfully.
+     */
+    protected boolean startWorkspaceDragOnLongClick(View view) {
+        View.DragShadowBuilder transparentShadowBuilder = new View.DragShadowBuilder(view) {
+            private static final int ARBITRARY_SHADOW_SIZE = 10;
+
+            @Override
+            public void onDrawShadow(Canvas canvas) {
+            }
+
+            @Override
+            public void onProvideShadowMetrics(Point outShadowSize, Point outShadowTouchPoint) {
+                outShadowSize.set(ARBITRARY_SHADOW_SIZE, ARBITRARY_SHADOW_SIZE);
+                outShadowTouchPoint.set(ARBITRARY_SHADOW_SIZE / 2, ARBITRARY_SHADOW_SIZE / 2);
+            }
+        };
+
+        TaskbarDragListener taskbarDragListener = new TaskbarDragListener(mLauncher,
+                (ItemInfo) view.getTag());
+        if (view.startDragAndDrop(new ClipData("", new String[] {taskbarDragListener.getMimeType()},
+                        new ClipData.Item("")),
+                transparentShadowBuilder, null /* localState */, View.DRAG_FLAG_GLOBAL)) {
+            view.setOnDragListener(getDraggedViewDragListener());
+            taskbarDragListener.init(mLauncher.getDragLayer());
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Hide the original Taskbar item while it is being dragged.
+     */
+    private View.OnDragListener getDraggedViewDragListener() {
+        return (view, dragEvent) -> {
+            switch (dragEvent.getAction()) {
+                case DragEvent.ACTION_DRAG_STARTED:
+                    view.setVisibility(INVISIBLE);
+                    return true;
+                case DragEvent.ACTION_DRAG_ENDED:
+                    view.setVisibility(VISIBLE);
+                    view.setOnDragListener(null);
+                    return true;
+            }
+            return false;
+        };
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragListener.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragListener.java
new file mode 100644
index 0000000..2bd5861
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragListener.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2021 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.taskbar;
+
+import android.content.ClipDescription;
+import android.graphics.Point;
+import android.view.DragEvent;
+import android.view.View;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.model.data.ItemInfo;
+
+import java.util.UUID;
+
+/**
+ * Listens to system drag and drop events initated by the Taskbar, and forwards them to Launcher's
+ * internal DragController to move Hotseat items.
+ */
+public class TaskbarDragListener implements View.OnDragListener {
+
+    private static final String MIME_TYPE_PREFIX = "com.android.launcher3.taskbar.drag_and_drop/";
+
+    private final BaseQuickstepLauncher mLauncher;
+    private final ItemInfo mDraggedItem;
+    private final DragOptions mDragOptions;
+    // Randomly generated id used to verify the drag event.
+    private final String mId;
+
+    // Initialized in init().
+    DragLayer mDragLayer;
+
+    /**
+     * @param draggedItem The info of the item that was long clicked, which we will use to find
+     *                    the equivalent match on Hotseat to drag internally.
+     */
+    public TaskbarDragListener(BaseQuickstepLauncher launcher, ItemInfo draggedItem) {
+        mLauncher = launcher;
+        mDraggedItem = draggedItem;
+        mDragOptions = new DragOptions();
+        mDragOptions.simulatedDndStartPoint = new Point();
+        mId = UUID.randomUUID().toString();
+    }
+
+    protected void init(DragLayer dragLayer) {
+        mDragLayer = dragLayer;
+        mDragLayer.setOnDragListener(this);
+    }
+
+    private void cleanup() {
+        mDragLayer.setOnDragListener(null);
+        mLauncher.setWorkspaceDragOptions(new DragOptions());
+    }
+
+    /**
+     * Returns a randomly generated id used to verify the drag event.
+     */
+    protected String getMimeType() {
+        return MIME_TYPE_PREFIX + mId;
+    }
+
+    @Override
+    public boolean onDrag(View dragLayer, DragEvent dragEvent) {
+        ClipDescription clipDescription = dragEvent.getClipDescription();
+        if (dragEvent.getAction() == DragEvent.ACTION_DRAG_STARTED) {
+            if (clipDescription == null || !clipDescription.hasMimeType(getMimeType())) {
+                // We didn't initiate this drag, ignore.
+                cleanup();
+                return false;
+            }
+            View hotseatView = mLauncher.getHotseat().getFirstItemMatch(
+                    (info, view) -> info == mDraggedItem);
+            if (hotseatView == null) {
+                cleanup();
+                return false;
+            }
+            mDragOptions.simulatedDndStartPoint.set((int) dragEvent.getX(), (int) dragEvent.getY());
+            mLauncher.setWorkspaceDragOptions(mDragOptions);
+            hotseatView.performLongClick();
+        } else if (dragEvent.getAction() == DragEvent.ACTION_DRAG_ENDED) {
+            cleanup();
+        }
+        return mLauncher.getDragController().onDragEvent(dragEvent);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java
new file mode 100644
index 0000000..082343e
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 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.taskbar;
+
+import android.view.View;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.ShortcutAndWidgetContainer;
+import com.android.launcher3.dragndrop.DragController;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.model.data.ItemInfo;
+
+/**
+ * Works with TaskbarController to update the TaskbarView's Hotseat items.
+ */
+public class TaskbarHotseatController {
+
+    private final BaseQuickstepLauncher mLauncher;
+    private final Hotseat mHotseat;
+    private final TaskbarController.TaskbarHotseatControllerCallbacks mTaskbarCallbacks;
+    private final int mNumHotseatIcons;
+
+    private final DragController.DragListener mDragListener = new DragController.DragListener() {
+        @Override
+        public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
+        }
+
+        @Override
+        public void onDragEnd() {
+            onHotseatUpdated();
+        }
+    };
+
+    public TaskbarHotseatController(BaseQuickstepLauncher launcher,
+            TaskbarController.TaskbarHotseatControllerCallbacks taskbarCallbacks) {
+        mLauncher = launcher;
+        mHotseat = mLauncher.getHotseat();
+        mTaskbarCallbacks = taskbarCallbacks;
+        mNumHotseatIcons = mLauncher.getDeviceProfile().inv.numHotseatIcons;
+    }
+
+    protected void init() {
+        mLauncher.getDragController().addDragListener(mDragListener);
+        onHotseatUpdated();
+    }
+
+    protected void cleanup() {
+        mLauncher.getDragController().removeDragListener(mDragListener);
+    }
+
+    /**
+     * Called when any Hotseat item changes, and reports the new list of items to TaskbarController.
+     */
+    protected void onHotseatUpdated() {
+        ShortcutAndWidgetContainer shortcutsAndWidgets = mHotseat.getShortcutsAndWidgets();
+        ItemInfo[] hotseatItemInfos = new ItemInfo[mNumHotseatIcons];
+        for (int i = 0; i < shortcutsAndWidgets.getChildCount(); i++) {
+            View child = shortcutsAndWidgets.getChildAt(i);
+            Object tag = shortcutsAndWidgets.getChildAt(i).getTag();
+            if (tag instanceof ItemInfo) {
+                ItemInfo itemInfo = (ItemInfo) tag;
+                CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
+                // Since the hotseat might be laid out vertically or horizontally, use whichever
+                // index is higher.
+                hotseatItemInfos[Math.max(lp.cellX, lp.cellY)] = itemInfo;
+            }
+        }
+
+        mTaskbarCallbacks.updateHotseatItems(hotseatItemInfos);
+    }
+
+    protected int getNumHotseatIcons() {
+        return mNumHotseatIcons;
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentsController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentsController.java
new file mode 100644
index 0000000..4256d2b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentsController.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2021 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.taskbar;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.util.CancellableTask;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
+
+import java.util.ArrayList;
+
+/**
+ * Works with TaskbarController to update the TaskbarView's Recent items.
+ */
+public class TaskbarRecentsController {
+
+    private final int mNumRecentIcons = 2;
+    private final BaseQuickstepLauncher mLauncher;
+    private final TaskbarController.TaskbarRecentsControllerCallbacks mTaskbarCallbacks;
+    private final RecentsModel mRecentsModel;
+
+    private final TaskStackChangeListener mTaskStackChangeListener = new TaskStackChangeListener() {
+        @Override
+        public void onTaskStackChanged() {
+            reloadRecentTasksIfNeeded();
+        }
+    };
+
+    // TODO: add TaskbarVisualsChangedListener as well (for calendar/clock?)
+
+    // Used to keep track of the last requested task list id, so that we do not request to load the
+    // tasks again if we have already requested it and the task list has not changed
+    private int mTaskListChangeId = -1;
+
+    // The current background requests to load the task icons
+    private CancellableTask[] mIconLoadRequests = new CancellableTask[mNumRecentIcons];
+
+    private boolean mIsAlive;
+
+    public TaskbarRecentsController(BaseQuickstepLauncher launcher,
+            TaskbarController.TaskbarRecentsControllerCallbacks taskbarCallbacks) {
+        mLauncher = launcher;
+        mTaskbarCallbacks = taskbarCallbacks;
+        mRecentsModel = RecentsModel.INSTANCE.get(mLauncher);
+    }
+
+    protected void init() {
+        mIsAlive = true;
+        TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackChangeListener);
+        reloadRecentTasksIfNeeded();
+    }
+
+    protected void cleanup() {
+        mIsAlive = false;
+        TaskStackChangeListeners.getInstance().unregisterTaskStackListener(
+                mTaskStackChangeListener);
+        cancelAllPendingIconLoadTasks();
+    }
+
+    private void reloadRecentTasksIfNeeded() {
+        if (!mRecentsModel.isTaskListValid(mTaskListChangeId)) {
+            mTaskListChangeId = mRecentsModel.getTasks(this::onRecentTasksChanged);
+        }
+    }
+
+    private void cancelAllPendingIconLoadTasks() {
+        for (int i = 0; i < mIconLoadRequests.length; i++) {
+            if (mIconLoadRequests[i] != null) {
+                mIconLoadRequests[i].cancel();
+            }
+            mIconLoadRequests[i] = null;
+        }
+    }
+
+    private void onRecentTasksChanged(ArrayList<Task> tasks) {
+        if (mIsAlive) {
+            mTaskbarCallbacks.updateRecentItems(tasks);
+        }
+    }
+
+    /**
+     * For each Task, loads its icon from the cache in the background, then calls
+     * {@link TaskbarController.TaskbarRecentsControllerCallbacks#updateRecentTaskAtIndex}.
+     */
+    protected void loadIconsForTasks(Task[] tasks) {
+        cancelAllPendingIconLoadTasks();
+        for (int i = 0; i < tasks.length; i++) {
+            Task task = tasks[i];
+            if (task == null) {
+                continue;
+            }
+            final int taskIndex = i;
+            mIconLoadRequests[i] = mRecentsModel.getIconCache().updateIconInBackground(
+                    task, updatedTask -> onTaskIconLoaded(task, taskIndex));
+        }
+    }
+
+    private void onTaskIconLoaded(Task task, int taskIndex) {
+        mTaskbarCallbacks.updateRecentTaskAtIndex(taskIndex, task);
+    }
+
+    protected int getNumRecentIcons() {
+        return mNumRecentIcons;
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStateHandler.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStateHandler.java
new file mode 100644
index 0000000..b4b5d8b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStateHandler.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2021 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.taskbar;
+
+import static com.android.launcher3.LauncherState.TASKBAR;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_TASKBAR_FADE;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_TASKBAR;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.states.StateAnimationConfig;
+import com.android.quickstep.AnimatedFloat;
+
+/**
+ * StateHandler to animate Taskbar according to Launcher's state machine. Does nothing if Taskbar
+ * isn't present (i.e. {@link #setTaskbarCallbacks} is never called).
+ */
+public class TaskbarStateHandler implements StateManager.StateHandler<LauncherState> {
+
+    private final BaseQuickstepLauncher mLauncher;
+
+    // Contains Taskbar-related methods and fields we should aniamte. If null, don't do anything.
+    private @Nullable TaskbarController.TaskbarStateHandlerCallbacks mTaskbarCallbacks = null;
+
+    public TaskbarStateHandler(BaseQuickstepLauncher launcher) {
+        mLauncher = launcher;
+    }
+
+    public void setTaskbarCallbacks(TaskbarController.TaskbarStateHandlerCallbacks callbacks) {
+        mTaskbarCallbacks = callbacks;
+    }
+
+    @Override
+    public void setState(LauncherState state) {
+        if (mTaskbarCallbacks == null) {
+            return;
+        }
+
+        AnimatedFloat alphaTarget = mTaskbarCallbacks.getAlphaTarget();
+        boolean isTaskbarVisible = (state.getVisibleElements(mLauncher) & TASKBAR) != 0;
+        alphaTarget.updateValue(isTaskbarVisible ? 1f : 0f);
+    }
+
+    @Override
+    public void setStateWithAnimation(LauncherState toState, StateAnimationConfig config,
+            PendingAnimation animation) {
+        if (mTaskbarCallbacks == null) {
+            return;
+        }
+        if (config.hasAnimationFlag(SKIP_TASKBAR)) {
+            return;
+        }
+
+        AnimatedFloat alphaTarget = mTaskbarCallbacks.getAlphaTarget();
+        boolean isTaskbarVisible = (toState.getVisibleElements(mLauncher) & TASKBAR) != 0;
+        animation.setFloat(alphaTarget, AnimatedFloat.VALUE, isTaskbarVisible ? 1f : 0f,
+                config.getInterpolator(ANIM_TASKBAR_FADE, Interpolators.LINEAR));
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 5df8d5f..a729e77 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -16,16 +16,67 @@
 package com.android.launcher3.taskbar;
 
 import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
+import android.view.DragEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
 import android.widget.LinearLayout;
 
+import androidx.annotation.LayoutRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.views.ActivityContext;
+import com.android.systemui.shared.recents.model.Task;
+
 /**
  * Hosts the Taskbar content such as Hotseat and Recent Apps. Drawn on top of other apps.
  */
-public class TaskbarView extends LinearLayout {
+public class TaskbarView extends LinearLayout implements FolderIcon.FolderIconParent {
+
+    private final ColorDrawable mBackgroundDrawable;
+    private final int mItemMarginLeftRight;
+    private final int mIconTouchSize;
+    private final boolean mIsRtl;
+    private final int mTouchSlop;
+    private final RectF mTempDelegateBounds = new RectF();
+    private final RectF mDelegateSlopBounds = new RectF();
+    private final int[] mTempOutLocation = new int[2];
+    private final Matrix mTempMatrix = new Matrix();
+
+    // Initialized in TaskbarController constructor.
+    private TaskbarController.TaskbarViewCallbacks mControllerCallbacks;
+
+    // Initialized in init().
+    private int mHotseatStartIndex;
+    private int mHotseatEndIndex;
+    private View mHotseatRecentsDivider;
+    private int mRecentsStartIndex;
+    private int mRecentsEndIndex;
+
+    // Delegate touches to the closest view if within mIconTouchSize.
+    private boolean mDelegateTargeted;
+    private View mDelegateView;
+
+    private boolean mIsDraggingItem;
+    // Only non-null when the corresponding Folder is open.
+    private @Nullable FolderIcon mLeaveBehindFolderIcon;
+
     public TaskbarView(@NonNull Context context) {
         this(context, null);
     }
@@ -42,5 +93,387 @@
     public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
+
+        Resources resources = getResources();
+        mBackgroundDrawable = (ColorDrawable) getBackground();
+        mItemMarginLeftRight = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing);
+        mIconTouchSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_touch_size);
+        mIsRtl = Utilities.isRtl(resources);
+        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+    }
+
+    protected void construct(TaskbarController.TaskbarViewCallbacks taskbarViewCallbacks) {
+        mControllerCallbacks = taskbarViewCallbacks;
+    }
+
+    protected void init(int numHotseatIcons, int numRecentIcons) {
+        mHotseatStartIndex = 0;
+        mHotseatEndIndex = mHotseatStartIndex + numHotseatIcons - 1;
+        updateHotseatItems(new ItemInfo[numHotseatIcons]);
+
+        int dividerIndex = mHotseatEndIndex + 1;
+        mHotseatRecentsDivider = addDivider(dividerIndex);
+
+        mRecentsStartIndex = dividerIndex + 1;
+        mRecentsEndIndex = mRecentsStartIndex + numRecentIcons - 1;
+        updateRecentTasks(new Task[numRecentIcons]);
+    }
+
+    protected void cleanup() {
+        removeAllViews();
+    }
+
+    /**
+     * Sets the alpha of the background color behind all the Taskbar contents.
+     * @param alpha 0 is fully transparent, 1 is fully opaque.
+     */
+    public void setBackgroundAlpha(float alpha) {
+        mBackgroundDrawable.setAlpha((int) (alpha * 255));
+    }
+
+    /**
+     * Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos.
+     */
+    protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
+        for (int i = 0; i < hotseatItemInfos.length; i++) {
+            ItemInfo hotseatItemInfo = hotseatItemInfos[!mIsRtl ? i
+                    : hotseatItemInfos.length - i - 1];
+            int hotseatIndex = mHotseatStartIndex + i;
+            View hotseatView = getChildAt(hotseatIndex);
+
+            // Replace any Hotseat views with the appropriate type if it's not already that type.
+            final int expectedLayoutResId;
+            boolean isFolder = false;
+            boolean needsReinflate = false;
+            if (hotseatItemInfo != null && hotseatItemInfo.isPredictedItem()) {
+                expectedLayoutResId = R.layout.taskbar_predicted_app_icon;
+            } else if (hotseatItemInfo instanceof FolderInfo) {
+                expectedLayoutResId = R.layout.folder_icon;
+                isFolder = true;
+                // Unlike for BubbleTextView, we can't reapply a new FolderInfo after inflation, so
+                // if the info changes we need to reinflate. This should only happen if a new folder
+                // is dragged to the position that another folder previously existed.
+                needsReinflate = hotseatView != null && hotseatView.getTag() != hotseatItemInfo;
+            } else {
+                expectedLayoutResId = R.layout.taskbar_app_icon;
+            }
+            if (hotseatView == null || hotseatView.getSourceLayoutResId() != expectedLayoutResId
+                    || needsReinflate) {
+                removeView(hotseatView);
+                TaskbarActivityContext activityContext =
+                        ActivityContext.lookupContext(getContext());
+                if (isFolder) {
+                    FolderInfo folderInfo = (FolderInfo) hotseatItemInfo;
+                    FolderIcon folderIcon = FolderIcon.inflateFolderAndIcon(expectedLayoutResId,
+                            activityContext, this, folderInfo);
+                    folderIcon.setTextVisible(false);
+                    hotseatView = folderIcon;
+                } else {
+                    hotseatView = inflate(expectedLayoutResId);
+                }
+                int iconSize = activityContext.getDeviceProfile().iconSizePx;
+                LayoutParams lp = new LayoutParams(iconSize, iconSize);
+                lp.setMargins(mItemMarginLeftRight, 0, mItemMarginLeftRight, 0);
+                addView(hotseatView, hotseatIndex, lp);
+            }
+
+            // Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
+            if (hotseatView instanceof BubbleTextView
+                    && hotseatItemInfo instanceof WorkspaceItemInfo) {
+                ((BubbleTextView) hotseatView).applyFromWorkspaceItem(
+                        (WorkspaceItemInfo) hotseatItemInfo);
+                hotseatView.setOnClickListener(mControllerCallbacks.getItemOnClickListener());
+                hotseatView.setOnLongClickListener(
+                        mControllerCallbacks.getItemOnLongClickListener());
+            } else if (isFolder) {
+                hotseatView.setOnClickListener(mControllerCallbacks.getItemOnClickListener());
+                hotseatView.setOnLongClickListener(
+                        mControllerCallbacks.getItemOnLongClickListener());
+            } else {
+                hotseatView.setOnClickListener(null);
+                hotseatView.setOnLongClickListener(null);
+                hotseatView.setTag(null);
+            }
+            updateHotseatItemVisibility(hotseatView);
+        }
+
+        updateHotseatRecentsDividerVisibility();
+    }
+
+    protected void updateHotseatItemsVisibility() {
+        for (int i = mHotseatStartIndex; i <= mHotseatEndIndex; i++) {
+            updateHotseatItemVisibility(getChildAt(i));
+        }
+    }
+
+    private void updateHotseatItemVisibility(View hotseatView) {
+        if (hotseatView.getTag() != null) {
+            hotseatView.setVisibility(VISIBLE);
+        } else {
+            int oldVisibility = hotseatView.getVisibility();
+            int newVisibility = mControllerCallbacks.getEmptyHotseatViewVisibility();
+            hotseatView.setVisibility(newVisibility);
+            if (oldVisibility == GONE && newVisibility != GONE) {
+                // By default, the layout transition only runs when going to VISIBLE,
+                // but we want it to run when going to GONE to INVISIBLE as well.
+                getLayoutTransition().showChild(this, hotseatView, oldVisibility);
+            }
+        }
+    }
+
+    private View addDivider(int dividerIndex) {
+        View divider = inflate(R.layout.taskbar_divider);
+        addView(divider, dividerIndex);
+        return divider;
+    }
+
+    /**
+     * Inflates/binds the Recents items to show in the Taskbar given their Tasks.
+     */
+    protected void updateRecentTasks(Task[] tasks) {
+        for (int i = 0; i < tasks.length; i++) {
+            Task task = tasks[i];
+            int recentsIndex = mRecentsStartIndex + i;
+            View recentsView = getChildAt(recentsIndex);
+
+            // Inflate empty icon Views.
+            if (recentsView == null) {
+                BubbleTextView btv = (BubbleTextView) inflate(R.layout.taskbar_app_icon);
+                LayoutParams lp = new LayoutParams(btv.getIconSize(), btv.getIconSize());
+                lp.setMargins(mItemMarginLeftRight, 0, mItemMarginLeftRight, 0);
+                recentsView = btv;
+                addView(recentsView, recentsIndex, lp);
+            }
+
+            // Apply the Task, or hide the view if there is none for a given index.
+            if (recentsView instanceof BubbleTextView && task != null) {
+                applyTaskToBubbleTextView((BubbleTextView) recentsView, task);
+                recentsView.setVisibility(VISIBLE);
+                recentsView.setOnClickListener(mControllerCallbacks.getItemOnClickListener());
+                recentsView.setOnLongClickListener(
+                        mControllerCallbacks.getItemOnLongClickListener());
+            } else {
+                recentsView.setVisibility(GONE);
+                recentsView.setOnClickListener(null);
+                recentsView.setOnLongClickListener(null);
+            }
+        }
+
+        updateHotseatRecentsDividerVisibility();
+    }
+
+    private void applyTaskToBubbleTextView(BubbleTextView btv, Task task) {
+        if (task.icon != null) {
+            Drawable icon = task.icon.getConstantState().newDrawable().mutate();
+            btv.applyIconAndLabel(icon, task.titleDescription);
+        }
+        btv.setTag(task);
+    }
+
+    protected void updateRecentTaskAtIndex(int taskIndex, Task task) {
+        View taskView = getChildAt(mRecentsStartIndex + taskIndex);
+        if (taskView instanceof BubbleTextView) {
+            applyTaskToBubbleTextView((BubbleTextView) taskView, task);
+        }
+    }
+
+    /**
+     * Make the divider VISIBLE between the Hotseat and Recents if there is at least one icon in
+     * each, otherwise make it GONE.
+     */
+    private void updateHotseatRecentsDividerVisibility() {
+        if (mHotseatRecentsDivider == null) {
+            return;
+        }
+
+        boolean hasAtLeastOneHotseatItem = false;
+        for (int i = mHotseatStartIndex; i <= mHotseatEndIndex; i++) {
+            if (getChildAt(i).getVisibility() != GONE) {
+                hasAtLeastOneHotseatItem = true;
+                break;
+            }
+        }
+
+        boolean hasAtLeastOneRecentItem = false;
+        for (int i = mRecentsStartIndex; i <= mRecentsEndIndex; i++) {
+            if (getChildAt(i).getVisibility() != GONE) {
+                hasAtLeastOneRecentItem = true;
+                break;
+            }
+        }
+
+        mHotseatRecentsDivider.setVisibility(hasAtLeastOneHotseatItem && hasAtLeastOneRecentItem
+                ? VISIBLE : GONE);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        boolean handled = delegateTouchIfNecessary(event);
+        return super.onTouchEvent(event) || handled;
+    }
+
+    /**
+     * User touched the Taskbar background. Determine whether the touch is close enough to a view
+     * that we should forward the touches to it.
+     * @return Whether a delegate view was chosen and it handled the touch event.
+     */
+    private boolean delegateTouchIfNecessary(MotionEvent event) {
+        final float x = event.getX();
+        final float y = event.getY();
+        if (mDelegateView == null && event.getAction() == MotionEvent.ACTION_DOWN) {
+            View delegateView = findDelegateView(x, y);
+            if (delegateView != null) {
+                mDelegateTargeted = true;
+                mDelegateView = delegateView;
+                mDelegateSlopBounds.set(mTempDelegateBounds);
+                mDelegateSlopBounds.inset(-mTouchSlop, -mTouchSlop);
+            }
+        }
+
+        boolean sendToDelegate = mDelegateTargeted;
+        boolean inBounds = true;
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_MOVE:
+                inBounds = mDelegateSlopBounds.contains(x, y);
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                mDelegateTargeted = false;
+                break;
+        }
+
+        boolean handled = false;
+        if (sendToDelegate) {
+            if (inBounds) {
+                // Offset event coordinates to be inside the target view
+                event.setLocation(mDelegateView.getWidth() / 2f, mDelegateView.getHeight() / 2f);
+            } else {
+                // Offset event coordinates to be outside the target view (in case it does
+                // something like tracking pressed state)
+                event.setLocation(-mTouchSlop * 2, -mTouchSlop * 2);
+            }
+            handled = mDelegateView.dispatchTouchEvent(event);
+            // Cleanup if this was the last event to send to the delegate.
+            if (!mDelegateTargeted) {
+                mDelegateView = null;
+            }
+        }
+        return handled;
+    }
+
+    /**
+     * Return an item whose touch bounds contain the given coordinates,
+     * or null if no such item exists.
+     *
+     * Also sets {@link #mTempDelegateBounds} to be the touch bounds of the chosen delegate view.
+     */
+    private @Nullable View findDelegateView(float x, float y) {
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            if (!child.isShown() || !child.isClickable()) {
+                continue;
+            }
+            int childCenterX = child.getLeft() + child.getWidth() / 2;
+            int childCenterY = child.getTop() + child.getHeight() / 2;
+            mTempDelegateBounds.set(
+                    childCenterX - mIconTouchSize / 2f,
+                    childCenterY - mIconTouchSize / 2f,
+                    childCenterX + mIconTouchSize / 2f,
+                    childCenterY + mIconTouchSize / 2f);
+            if (mTempDelegateBounds.contains(x, y)) {
+                return child;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's
+     * touch bounds.
+     */
+    public boolean isEventOverAnyItem(MotionEvent ev) {
+        getLocationOnScreen(mTempOutLocation);
+        float xInOurCoordinates = ev.getX() - mTempOutLocation[0];
+        float yInOurCoorindates = ev.getY() - mTempOutLocation[1];
+        return findDelegateView(xInOurCoordinates, yInOurCoorindates) != null;
+    }
+
+    @Override
+    public boolean onDragEvent(DragEvent event) {
+        switch (event.getAction()) {
+            case DragEvent.ACTION_DRAG_STARTED:
+                mIsDraggingItem = true;
+                AbstractFloatingView.closeAllOpenViews(ActivityContext.lookupContext(getContext()));
+                return true;
+            case DragEvent.ACTION_DRAG_ENDED:
+                mIsDraggingItem = false;
+                break;
+        }
+        return super.onDragEvent(event);
+    }
+
+    public boolean isDraggingItem() {
+        return mIsDraggingItem;
+    }
+
+    /**
+     * @return The bounding box of where the hotseat elements will be when we reach the given scale.
+     */
+    protected RectF getHotseatBoundsAtScale(float taskbarViewScale) {
+        View firstHotseatView = null, lastHotseatView = null;
+        for (int i = mHotseatStartIndex; i <= mHotseatEndIndex; i++) {
+            View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                if (firstHotseatView == null) {
+                    firstHotseatView = child;
+                }
+                lastHotseatView = child;
+            }
+        }
+        if (firstHotseatView == null || lastHotseatView == null) {
+            return new RectF();
+        }
+        View leftmostHotseatView = !mIsRtl ? firstHotseatView : lastHotseatView;
+        View rightmostHotseatView = !mIsRtl ? lastHotseatView : firstHotseatView;
+        RectF hotseatBounds = new RectF(
+                leftmostHotseatView.getLeft() - mItemMarginLeftRight,
+                leftmostHotseatView.getTop(),
+                rightmostHotseatView.getRight() + mItemMarginLeftRight,
+                rightmostHotseatView.getBottom());
+        mTempMatrix.setScale(taskbarViewScale, taskbarViewScale, getPivotX(), getPivotY());
+        mTempMatrix.mapRect(hotseatBounds);
+        return hotseatBounds;
+    }
+
+    // FolderIconParent implemented methods.
+
+    @Override
+    public void drawFolderLeaveBehindForIcon(FolderIcon child) {
+        mLeaveBehindFolderIcon = child;
+        invalidate();
+    }
+
+    @Override
+    public void clearFolderLeaveBehind(FolderIcon child) {
+        mLeaveBehindFolderIcon = null;
+        invalidate();
+    }
+
+    // End FolderIconParent implemented methods.
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        if (mLeaveBehindFolderIcon != null) {
+            canvas.save();
+            canvas.translate(mLeaveBehindFolderIcon.getLeft(), mLeaveBehindFolderIcon.getTop());
+            mLeaveBehindFolderIcon.getFolderBackground().drawLeaveBehind(canvas);
+            canvas.restore();
+        }
+    }
+
+    private View inflate(@LayoutRes int layoutResId) {
+        TaskbarActivityContext taskbarActivityContext = ActivityContext.lookupContext(getContext());
+        return taskbarActivityContext.getLayoutInflater().inflate(layoutResId, this, false);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarVisibilityController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarVisibilityController.java
new file mode 100644
index 0000000..6d20d97
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarVisibilityController.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2021 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.taskbar;
+
+import static com.android.launcher3.LauncherState.TASKBAR;
+
+import android.animation.Animator;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.SystemUiProxy;
+import com.android.systemui.shared.system.QuickStepContract;
+
+/**
+ * Works with TaskbarController to update the TaskbarView's alpha based on LauncherState, whether
+ * Launcher is in the foreground, etc.
+ */
+public class TaskbarVisibilityController {
+
+    private static final long IME_VISIBILITY_ALPHA_DURATION = 120;
+
+    private final BaseQuickstepLauncher mLauncher;
+    private final TaskbarController.TaskbarVisibilityControllerCallbacks mTaskbarCallbacks;
+
+    // Background alpha.
+    private AnimatedFloat mTaskbarBackgroundAlpha = new AnimatedFloat(
+            this::onTaskbarBackgroundAlphaChanged);
+
+    // Overall visibility.
+    private AnimatedFloat mTaskbarVisibilityAlphaForLauncherState = new AnimatedFloat(
+            this::updateVisibilityAlpha);
+    private AnimatedFloat mTaskbarVisibilityAlphaForIme = new AnimatedFloat(
+            this::updateVisibilityAlpha);
+
+    public TaskbarVisibilityController(BaseQuickstepLauncher launcher,
+            TaskbarController.TaskbarVisibilityControllerCallbacks taskbarCallbacks) {
+        mLauncher = launcher;
+        mTaskbarCallbacks = taskbarCallbacks;
+    }
+
+    protected void init() {
+        mTaskbarBackgroundAlpha.updateValue(mLauncher.hasBeenResumed() ? 0f : 1f);
+        boolean isVisibleForLauncherState = (mLauncher.getStateManager().getState()
+                .getVisibleElements(mLauncher) & TASKBAR) != 0;
+        mTaskbarVisibilityAlphaForLauncherState.updateValue(isVisibleForLauncherState ? 1f : 0f);
+        boolean isImeVisible = (SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags()
+                & QuickStepContract.SYSUI_STATE_IME_SHOWING) != 0;
+        mTaskbarVisibilityAlphaForIme.updateValue(isImeVisible ? 0f : 1f);
+
+        onTaskbarBackgroundAlphaChanged();
+        updateVisibilityAlpha();
+    }
+
+    protected void cleanup() {
+    }
+
+    protected AnimatedFloat getTaskbarVisibilityForLauncherState() {
+        return mTaskbarVisibilityAlphaForLauncherState;
+    }
+
+    protected Animator createAnimToBackgroundAlpha(float toAlpha, long duration) {
+        return mTaskbarBackgroundAlpha.animateToValue(mTaskbarBackgroundAlpha.value, toAlpha)
+                .setDuration(duration);
+    }
+
+    protected void animateToVisibilityForIme(float toAlpha) {
+        mTaskbarVisibilityAlphaForIme.animateToValue(mTaskbarVisibilityAlphaForIme.value, toAlpha)
+                .setDuration(IME_VISIBILITY_ALPHA_DURATION).start();
+    }
+
+    private void onTaskbarBackgroundAlphaChanged() {
+        mTaskbarCallbacks.updateTaskbarBackgroundAlpha(mTaskbarBackgroundAlpha.value);
+        updateVisibilityAlpha();
+    }
+
+    private void updateVisibilityAlpha() {
+        // We use mTaskbarBackgroundAlpha as a proxy for whether Launcher is resumed/paused, the
+        // assumption being that Taskbar should always be visible regardless of the current
+        // LauncherState if Launcher is paused.
+        float alphaDueToLauncher = Math.max(mTaskbarBackgroundAlpha.value,
+                mTaskbarVisibilityAlphaForLauncherState.value);
+        float alphaDueToOther = mTaskbarVisibilityAlphaForIme.value;
+        mTaskbarCallbacks.updateTaskbarVisibilityAlpha(alphaDueToLauncher * alphaDueToOther);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index aad7e17..0f13ef9 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -30,6 +30,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
+import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 
@@ -73,6 +74,8 @@
         SCRIM_PROGRESS.set(scrim, state.getOverviewScrimAlpha(mLauncher));
         SCRIM_MULTIPLIER.set(scrim, 1f);
         getTaskModalnessProperty().set(mRecentsView, state.getOverviewModalness());
+        RECENTS_GRID_PROGRESS.set(mRecentsView, state.displayOverviewTasksAsGrid(mLauncher)
+                ? 1f : 0f);
     }
 
     @Override
@@ -117,6 +120,8 @@
                 mRecentsView, getTaskModalnessProperty(),
                 toState.getOverviewModalness(),
                 config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
+        setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS,
+                toState.displayOverviewTasksAsGrid(mLauncher) ? 1f : 0f, LINEAR);
     }
 
     abstract FloatProperty getTaskModalnessProperty();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java b/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
index 010694b..bb1f6fc 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
@@ -60,6 +60,14 @@
         mListeners.add(r);
     }
 
+    @Override
+    public void removeChangeListener(Runnable r) {
+        if (mListeners == null) {
+            return;
+        }
+        mListeners.remove(r);
+    }
+
     private void registerDeviceConfigChangedListener(Context context) {
         DeviceConfig.addOnPropertiesChangedListener(
                 NAMESPACE_LAUNCHER,
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 597c17b..d330a68 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.uioverrides;
 
-import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.PIN_PREDICTION;
 import static com.android.launcher3.graphics.IconShape.getShape;
 
 import android.content.Context;
@@ -29,7 +28,6 @@
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.core.graphics.ColorUtils;
 
@@ -37,9 +35,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.graphics.IconPalette;
-import com.android.launcher3.hybridhotseat.HotseatPredictionController;
 import com.android.launcher3.icons.IconNormalizer;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.model.data.ItemInfo;
@@ -53,8 +49,7 @@
 /**
  * A BubbleTextView with a ring around it's drawable
  */
-public class PredictedAppIcon extends DoubleShadowBubbleTextView implements
-        LauncherAccessibilityDelegate.AccessibilityActionHandler {
+public class PredictedAppIcon extends DoubleShadowBubbleTextView {
 
     private static final int RING_SHADOW_COLOR = 0x99000000;
     private static final float RING_EFFECT_RATIO = 0.095f;
@@ -148,29 +143,6 @@
     }
 
     @Override
-    public void addSupportedAccessibilityActions(AccessibilityNodeInfo accessibilityNodeInfo) {
-        if (!mIsPinned) {
-            accessibilityNodeInfo.addAction(
-                    new AccessibilityNodeInfo.AccessibilityAction(PIN_PREDICTION,
-                            getContext().getText(R.string.pin_prediction)));
-        }
-    }
-
-    @Override
-    public boolean performAccessibilityAction(int action, ItemInfo info) {
-        QuickstepLauncher launcher = Launcher.cast(Launcher.getLauncher(getContext()));
-        if (action == PIN_PREDICTION) {
-            if (launcher == null || launcher.getHotseatPredictionController() == null) {
-                return false;
-            }
-            HotseatPredictionController controller = launcher.getHotseatPredictionController();
-            controller.pinPrediction(info);
-            return true;
-        }
-        return false;
-    }
-
-    @Override
     public void getIconBounds(Rect outBounds) {
         super.getIconBounds(outBounds);
         if (!mIsPinned && !mIsDrawingDot) {
@@ -179,12 +151,19 @@
         }
     }
 
+    public boolean isPinned() {
+        return mIsPinned;
+    }
+
     private int getOutlineOffsetX() {
         return (getMeasuredWidth() / 2) - mNormalizedIconRadius;
     }
 
     private int getOutlineOffsetY() {
-        return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
+        if (mDisplay != DISPLAY_TASKBAR) {
+            return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
+        }
+        return (getMeasuredHeight() / 2) - mNormalizedIconRadius;
     }
 
     private void drawEffect(Canvas canvas, boolean isBadged) {
@@ -249,17 +228,13 @@
      */
     public static class PredictedIconOutlineDrawing extends CellLayout.DelegatedCellDrawing {
 
-        private int mOffsetX;
-        private int mOffsetY;
-        private int mIconRadius;
-        private Paint mOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        private final PredictedAppIcon mIcon;
+        private final Paint mOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 
         public PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon) {
             mDelegateCellX = cellX;
             mDelegateCellY = cellY;
-            mOffsetX = icon.getOutlineOffsetX();
-            mOffsetY = icon.getOutlineOffsetY();
-            mIconRadius = icon.mNormalizedIconRadius;
+            mIcon = icon;
             mOutlinePaint.setStyle(Paint.Style.FILL);
             mOutlinePaint.setColor(Color.argb(24, 245, 245, 245));
         }
@@ -269,7 +244,8 @@
          */
         @Override
         public void drawUnderItem(Canvas canvas) {
-            getShape().drawShape(canvas, mOffsetX, mOffsetY, mIconRadius, mOutlinePaint);
+            getShape().drawShape(canvas, mIcon.getOutlineOffsetX(), mIcon.getOutlineOffsetY(),
+                    mIcon.mNormalizedIconRadius, mOutlinePaint);
         }
 
         /**
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index a00ce56..b009629 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -17,9 +17,11 @@
 
 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
 
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
@@ -41,19 +43,18 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.QuickstepAccessibilityDelegate;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.allapps.AllAppsContainerView;
-import com.android.launcher3.allapps.search.SearchAdapterProvider;
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.appprediction.PredictionRowView;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.hybridhotseat.HotseatPredictionController;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.search.DeviceSearchAdapterProvider;
 import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
 import com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory;
 import com.android.launcher3.uioverrides.touchcontrollers.NavBarToHomeTouchController;
@@ -80,6 +81,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Objects;
 import java.util.stream.Stream;
 
@@ -98,13 +100,18 @@
     @Override
     protected void setupViews() {
         super.setupViews();
-        if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get()) {
-            mHotseatPredictionController = new HotseatPredictionController(this);
-        }
+        mHotseatPredictionController = new HotseatPredictionController(this);
     }
 
     @Override
     protected void logAppLaunch(ItemInfo info, InstanceId instanceId) {
+        // If the app launch is from any of the surfaces in AllApps then add the InstanceId from
+        // LiveSearchManager to recreate the AllApps session on the server side.
+        if (mAllAppsSessionLogId != null && ALL_APPS.equals(
+                getStateManager().getCurrentStableState())) {
+            instanceId = mAllAppsSessionLogId;
+        }
+
         StatsLogger logger = getStatsLogManager()
                 .logger().withItemInfo(info).withInstanceId(instanceId);
 
@@ -126,9 +133,12 @@
         }
         logger.log(LAUNCHER_APP_LAUNCH_TAP);
 
-        if (mHotseatPredictionController != null) {
-            mHotseatPredictionController.logLaunchedAppRankingInfo(info, instanceId);
-        }
+        mHotseatPredictionController.logLaunchedAppRankingInfo(info, instanceId);
+    }
+
+    @Override
+    protected LauncherAccessibilityDelegate createAccessibilityDelegate() {
+        return new QuickstepAccessibilityDelegate(this);
     }
 
     /**
@@ -151,9 +161,8 @@
 
     @Override
     public boolean startActivitySafely(View v, Intent intent, ItemInfo item) {
-        if (mHotseatPredictionController != null) {
-            mHotseatPredictionController.setPauseUIUpdate(true);
-        }
+        // Only pause is taskbar controller is not present
+        mHotseatPredictionController.setPauseUIUpdate(getTaskbarController() == null);
         return super.startActivitySafely(v, intent, item);
     }
 
@@ -165,7 +174,7 @@
             onStateOrResumeChanging((getActivityFlags() & ACTIVITY_STATE_TRANSITION_ACTIVE) == 0);
         }
 
-        if (mHotseatPredictionController != null && ((changeBits & ACTIVITY_STATE_STARTED) != 0
+        if (((changeBits & ACTIVITY_STATE_STARTED) != 0
                 || (changeBits & getActivityFlags() & ACTIVITY_STATE_DEFERRED_RESUMED) != 0)) {
             mHotseatPredictionController.setPauseUIUpdate(false);
         }
@@ -179,12 +188,8 @@
 
     @Override
     public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
-        if (mHotseatPredictionController != null) {
-            return Stream.concat(super.getSupportedShortcuts(),
-                    Stream.of(mHotseatPredictionController));
-        } else {
-            return super.getSupportedShortcuts();
-        }
+        return Stream.concat(
+                super.getSupportedShortcuts(), Stream.of(mHotseatPredictionController));
     }
 
     /**
@@ -211,9 +216,19 @@
             mAllAppsPredictions = item;
             getAppsView().getFloatingHeaderView().findFixedRowByType(PredictionRowView.class)
                     .setPredictedApps(item.items);
-        } else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION
-                && mHotseatPredictionController != null) {
+        } else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) {
             mHotseatPredictionController.setPredictedItems(item);
+        } else if (item.containerId == Favorites.CONTAINER_WIDGETS_PREDICTION) {
+            getPopupDataProvider().setRecommendedWidgets(item.items);
+        }
+    }
+
+    @Override
+    public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) {
+        super.bindWorkspaceItemsChanged(updated);
+        if (getTaskbarController() != null && updated.stream()
+                .filter(w -> w.container == CONTAINER_HOTSEAT).findFirst().isPresent()) {
+            getTaskbarController().onHotseatUpdated();
         }
     }
 
@@ -221,10 +236,7 @@
     public void onDestroy() {
         super.onDestroy();
         getAppsView().getSearchUiManager().destroy();
-        if (mHotseatPredictionController != null) {
-            mHotseatPredictionController.destroy();
-            mHotseatPredictionController = null;
-        }
+        mHotseatPredictionController.destroy();
     }
 
     @Override
@@ -268,11 +280,6 @@
     }
 
     @Override
-    public SearchAdapterProvider createSearchAdapterProvider(AllAppsContainerView appsView) {
-        return new DeviceSearchAdapterProvider(this, appsView);
-    }
-
-    @Override
     public TouchController[] createTouchControllers() {
         Mode mode = SysUINavigationMode.getMode(this);
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 5ccc1e8..c9de662 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -15,7 +15,8 @@
  */
 package com.android.launcher3.uioverrides;
 
-import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
+import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON;
+import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE;
 import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
@@ -83,11 +84,14 @@
 
     private void setAlphas(PropertySetter propertySetter, StateAnimationConfig config,
             LauncherState state) {
-        float buttonAlpha = (state.getVisibleElements(mLauncher) & OVERVIEW_BUTTONS) != 0 ? 1 : 0;
+        float clearAllButtonAlpha = (state.getVisibleElements(mLauncher) & CLEAR_ALL_BUTTON) != 0
+                ? 1 : 0;
         propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
-                buttonAlpha, LINEAR);
+                clearAllButtonAlpha, LINEAR);
+        float overviewButtonAlpha = (state.getVisibleElements(mLauncher) & OVERVIEW_ACTIONS) != 0
+                ? 1 : 0;
         propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
-                MultiValueAlpha.VALUE, buttonAlpha, config.getInterpolator(
+                MultiValueAlpha.VALUE, overviewButtonAlpha, config.getInterpolator(
                         ANIM_OVERVIEW_ACTIONS_FADE, LINEAR));
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index 4b4f955..2ad718b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -69,7 +69,15 @@
     @Override
     public int getVisibleElements(Launcher launcher) {
         return super.getVisibleElements(launcher)
-                & ~OVERVIEW_BUTTONS & ~VERTICAL_SWIPE_INDICATOR;
+                & ~OVERVIEW_ACTIONS
+                & ~CLEAR_ALL_BUTTON
+                & ~VERTICAL_SWIPE_INDICATOR
+                | TASKBAR;
+    }
+
+    @Override
+    public boolean displayOverviewTasksAsGrid(Launcher launcher) {
+        return false;
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
index 41c689d..bdba482 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
@@ -45,7 +45,7 @@
 
     @Override
     public int getVisibleElements(Launcher launcher) {
-        return OVERVIEW_BUTTONS;
+        return OVERVIEW_ACTIONS | CLEAR_ALL_BUTTON;
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index b295e79..d480b6d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -28,6 +28,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.views.RecentsView;
@@ -122,7 +123,8 @@
 
     @Override
     public int getVisibleElements(Launcher launcher) {
-        return OVERVIEW_BUTTONS;
+        return displayOverviewTasksAsGrid(launcher) ? CLEAR_ALL_BUTTON
+                : CLEAR_ALL_BUTTON | OVERVIEW_ACTIONS;
     }
 
     @Override
@@ -131,6 +133,11 @@
     }
 
     @Override
+    public boolean displayOverviewTasksAsGrid(Launcher launcher) {
+        return launcher.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get();
+    }
+
+    @Override
     public String getDescription(Launcher launcher) {
         return launcher.getString(R.string.accessibility_recent_apps);
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
index 51e72da..965f474 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
@@ -40,6 +40,6 @@
 
     @Override
     public int getVisibleElements(Launcher launcher) {
-        return NONE;
+        return TASKBAR;
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index efb91c6..473fe2d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -18,6 +18,7 @@
 import static android.view.View.VISIBLE;
 
 import static com.android.launcher3.LauncherState.HINT_STATE;
+import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.WorkspaceStateTransitionAnimation.getSpringScaleAnimator;
@@ -32,10 +33,12 @@
 import static com.android.launcher3.anim.Interpolators.clampToProgress;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_TASKBAR_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
@@ -47,7 +50,6 @@
 
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.Hotseat;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.allapps.AllAppsContainerView;
@@ -61,7 +63,7 @@
  * Animation factory for quickstep specific transitions
  */
 public class QuickstepAtomicAnimationFactory extends
-        RecentsAtomicAnimationFactory<Launcher, LauncherState> {
+        RecentsAtomicAnimationFactory<QuickstepLauncher, LauncherState> {
 
     // Scale recents takes before animating in
     private static final float RECENTS_PREPARE_SCALE = 1.33f;
@@ -80,6 +82,7 @@
         if (toState == NORMAL && fromState == OVERVIEW) {
             config.setInterpolator(ANIM_WORKSPACE_SCALE, DEACCEL);
             config.setInterpolator(ANIM_WORKSPACE_FADE, ACCEL);
+            config.setInterpolator(ANIM_TASKBAR_FADE, ACCEL);
             config.setInterpolator(ANIM_ALL_APPS_FADE, ACCEL);
             config.setInterpolator(ANIM_OVERVIEW_SCALE, clampToProgress(ACCEL, 0, 0.9f));
             config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, ACCEL_DEACCEL);
@@ -138,6 +141,7 @@
             config.setInterpolator(ANIM_DEPTH, OVERSHOOT_1_2);
             config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_X, OVERSHOOT_1_2);
             config.setInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, OVERSHOOT_1_2);
+            config.setInterpolator(ANIM_TASKBAR_FADE, OVERSHOOT_1_2);
         } else if (fromState == HINT_STATE && toState == NORMAL) {
             config.setInterpolator(ANIM_DEPTH, DEACCEL_3);
             if (mHintToNormalDuration == -1) {
@@ -146,6 +150,17 @@
                 mHintToNormalDuration = (int) va.getDuration();
             }
             config.duration = Math.max(config.duration, mHintToNormalDuration);
+        } else if (mActivity.getTaskbarController() != null)  {
+            boolean wasHotseatVisible = fromState.areElementsVisible(mActivity, HOTSEAT_ICONS);
+            boolean isHotseatVisible = toState.areElementsVisible(mActivity, HOTSEAT_ICONS);
+            if (wasHotseatVisible || isHotseatVisible) {
+                config.setInterpolator(ANIM_TASKBAR_FADE, INSTANT);
+                config.setInterpolator(ANIM_HOTSEAT_FADE, INSTANT);
+
+                if (isHotseatVisible) {
+                    mActivity.getTaskbarController().alignRealHotseatWithTaskbar();
+                }
+            }
         }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index 6b9c340..a990f3e 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -24,8 +24,8 @@
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_EDU;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.animation.ValueAnimator;
@@ -145,7 +145,7 @@
                     OverviewScrim.SCRIM_MULTIPLIER, OVERVIEW_TO_HOME_SCRIM_MULTIPLIER,
                     PULLBACK_INTERPOLATOR);
 
-            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            if (LIVE_TILE.get()) {
                 builder.addOnFrameCallback(recentsView::redrawLiveTile);
             }
 
@@ -194,7 +194,7 @@
         boolean success = interpolatedProgress >= SUCCESS_TRANSITION_PROGRESS
                 || (velocity < 0 && fling);
         if (success) {
-            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            if (LIVE_TILE.get()) {
                 RecentsView recentsView = mLauncher.getOverviewPanel();
                 recentsView.switchToScreenshot(null,
                         () -> recentsView.finishRecentsAnimation(true /* toRecents */, null));
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index df433f8..4766870 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -18,7 +18,7 @@
 import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
+import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
 import static com.android.launcher3.LauncherState.QUICK_SWITCH;
 import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD;
 import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
@@ -222,7 +222,7 @@
         mRecentsView.setContentAlpha(1);
         mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
         mLauncher.getActionsView().getVisibilityAlpha().setValue(
-                (fromState.getVisibleElements(mLauncher) & OVERVIEW_BUTTONS) != 0 ? 1f : 0f);
+                (fromState.getVisibleElements(mLauncher) & OVERVIEW_ACTIONS) != 0 ? 1f : 0f);
 
         float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
         // As we drag right, animate the following properties:
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
index 845699a..4df0f63 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
@@ -76,7 +76,7 @@
      * @return the animation
      */
     PendingAnimation createSwipeDownToTaskAppAnimation(long duration, Interpolator interpolator) {
-        mRecentsView.setCurrentPage(mRecentsView.getPageNearestToCenterOfScreen());
+        mRecentsView.setCurrentPage(mRecentsView.getDestinationPage());
         TaskView taskView = mRecentsView.getCurrentPageTaskView();
         if (taskView == null) {
             throw new IllegalStateException("There is no task view to animate to.");
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 73f4ff2..facfb9d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -49,6 +49,7 @@
 import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 
 /**
  * Touch controller for handling various state transitions in portrait UI.
@@ -125,26 +126,11 @@
 
     @Override
     protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "PortraitStatesTouchController.getTargetState");
-        }
         if (fromState == ALL_APPS && !isDragTowardPositive) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
-                        "PortraitStatesTouchController.getTargetState 1");
-            }
             return NORMAL;
         } else if (fromState == OVERVIEW) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
-                        "PortraitStatesTouchController.getTargetState 2");
-            }
             return isDragTowardPositive ? OVERVIEW : NORMAL;
         } else if (fromState == NORMAL && isDragTowardPositive) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
-                        "PortraitStatesTouchController.getTargetState 3");
-            }
             return ALL_APPS;
         }
         return fromState;
@@ -319,4 +305,44 @@
             return baseInterpolator.getInterpolation(v);
         }
     }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        switch (ev.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                InteractionJankMonitorWrapper.begin(
+                        mLauncher.getRootView(), InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
+                break;
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                InteractionJankMonitorWrapper.cancel(
+                        InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
+                break;
+        }
+        return super.onControllerInterceptTouchEvent(ev);
+
+    }
+
+    @Override
+    protected void onReinitToState(LauncherState newToState) {
+        super.onReinitToState(newToState);
+        if (newToState != ALL_APPS) {
+            InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
+        }
+    }
+
+    @Override
+    protected void onReachedFinalState(LauncherState toState) {
+        super.onReinitToState(toState);
+        if (toState == ALL_APPS) {
+            InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
+        }
+    }
+
+    @Override
+    protected void clearState() {
+        super.clearState();
+        InteractionJankMonitorWrapper.cancel(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index 7675a79..c2e5cda 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -32,6 +32,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.touch.BaseSwipeDetector;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
@@ -150,9 +151,11 @@
                         mTaskBeingDragged = view;
                         int upDirection = mRecentsView.getPagedOrientationHandler()
                                 .getUpDirection(mIsRtl);
-                        if (!SysUINavigationMode.getMode(mActivity).hasGestures) {
+                        if (!SysUINavigationMode.getMode(mActivity).hasGestures || (
+                                mActivity.getDeviceProfile().isTablet
+                                        && FeatureFlags.ENABLE_OVERVIEW_GRID.get())) {
                             // Don't allow swipe down to open if we don't support swipe up
-                            // to enter overview.
+                            // to enter overview, or when grid layout is enabled.
                             directionsToDetectScroll = upDirection;
                             mAllowGoingUp = true;
                             mAllowGoingDown = false;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java
index 6271a44..faf5054 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java
@@ -31,6 +31,7 @@
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.views.AllAppsEduView;
 
 /**
@@ -92,7 +93,8 @@
                     mLauncher.getDeviceProfile().isSeascape() == isDragTowardPositive;
             return draggingFromNav ? OVERVIEW : NORMAL;
         } else {
-            return isDragTowardPositive ^ (fromState == OVERVIEW) ? OVERVIEW : NORMAL;
+            LauncherState startState = mStartState != null ? mStartState : fromState;
+            return isDragTowardPositive ^ (startState == OVERVIEW) ? OVERVIEW : NORMAL;
         }
     }
 
@@ -106,8 +108,8 @@
 
     @Override
     protected float getShiftRange() {
-        return mLauncher.getDeviceProfile().isVerticalBarLayout()
-                ? mLauncher.getDragLayer().getWidth() : super.getShiftRange();
+        // Should be in sync with TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT
+        return LayoutUtils.getDefaultSwipeHeight(mLauncher, mLauncher.getDeviceProfile());
     }
 
     @Override
@@ -116,7 +118,7 @@
         long maxAccuracy = (long) (2 * range);
         mCurrentAnimation = mLauncher.getStateManager().createAnimationToNewWorkspace(mToState,
                 maxAccuracy, animComponent);
-        return (mLauncher.getDeviceProfile().isSeascape() ? 2 : -2) / range;
+        return (mLauncher.getDeviceProfile().isSeascape() ? 1 : -1) / range;
     }
 
     @Override
@@ -134,5 +136,6 @@
                 AllAppsEduView.show(mLauncher);
             }
         }
+        mStartState = null;
     }
 }
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 39a3a7c..e0c041e 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -26,7 +26,6 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 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.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
@@ -46,12 +45,15 @@
 import static com.android.quickstep.GestureState.STATE_END_TARGET_SET;
 import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
+import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
@@ -78,7 +80,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.statemanager.StatefulActivity;
@@ -102,7 +104,6 @@
 import com.android.quickstep.util.SurfaceTransactionApplier;
 import com.android.quickstep.util.SwipePipToHomeAnimator;
 import com.android.quickstep.util.TransformParams;
-import com.android.quickstep.views.LiveTileOverlay;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -316,7 +317,7 @@
         mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED | STATE_FINISH_WITH_NO_END,
                 this::notifyTransitionCancelled);
 
-        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (!LIVE_TILE.get()) {
             mStateCallback.addChangeListener(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT
                             | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT,
                     (b) -> mRecentsView.setRunningTaskHidden(!b));
@@ -351,7 +352,6 @@
 
         mRecentsView = activity.getOverviewPanel();
         mRecentsView.setOnPageTransitionEndCallback(null);
-        addLiveTileOverlay();
 
         mStateCallback.setState(STATE_LAUNCHER_PRESENT);
         if (alreadyOnHome) {
@@ -457,7 +457,7 @@
     }
 
     private void onDeferredActivityLaunch() {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             mActivityInterface.switchRunningTaskViewToScreenshot(
                     null, () -> {
                         mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */);
@@ -731,7 +731,6 @@
         setIsLikelyToStartNewTask(isLikelyToStartNewTask, false /* animate */);
         mStateCallback.setStateOnUiThread(STATE_GESTURE_STARTED);
         mGestureStarted = true;
-        mTaskViewSimulator.setDrawsBelowRecents(true);
     }
 
     /**
@@ -917,26 +916,15 @@
                 isFling, isCancel);
         float endShift = endTarget.isLauncher ? 1 : 0;
         final float startShift;
-        Interpolator interpolator = DEACCEL;
         if (!isFling) {
             long expectedDuration = Math.abs(Math.round((endShift - currentShift)
                     * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
             duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
             startShift = currentShift;
-            interpolator = endTarget == RECENTS ? OVERSHOOT_1_2 : DEACCEL;
         } else {
             startShift = Utilities.boundToRange(currentShift - velocity.y
                     * getSingleFrameMs(mContext) / mTransitionDragLength, 0, mDragLengthFactor);
             if (mTransitionDragLength > 0) {
-                if (endTarget == RECENTS && !mDeviceState.isFullyGesturalNavMode()) {
-                    Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams(
-                            startShift, endShift, endShift, endVelocity,
-                            mTransitionDragLength, mContext);
-                    endShift = overshoot.end;
-                    interpolator = overshoot.interpolator;
-                    duration = Utilities.boundToRange(overshoot.duration, MIN_OVERSHOOT_DURATION,
-                            MAX_SWIPE_DURATION);
-                } else {
                     float distanceToTravel = (endShift - currentShift) * mTransitionDragLength;
 
                     // we want the page's snap velocity to approximately match the velocity at
@@ -944,13 +932,11 @@
                     // derivative of the scroll interpolator at zero, ie. 2.
                     long baseDuration = Math.round(Math.abs(distanceToTravel / velocity.y));
                     duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
-
-                    if (endTarget == RECENTS) {
-                        interpolator = OVERSHOOT_1_2;
-                    }
-                }
             }
         }
+        Interpolator interpolator =
+                endTarget == RECENTS ? (mDp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()
+                        ? ACCEL_DEACCEL : OVERSHOOT_1_2) : DEACCEL;
 
         if (endTarget.isLauncher) {
             mInputConsumerProxy.enable();
@@ -958,9 +944,8 @@
         if (endTarget == HOME) {
             duration = Math.max(MIN_OVERSHOOT_DURATION, duration);
         } else if (endTarget == RECENTS) {
-            LiveTileOverlay.INSTANCE.startIconAnimation();
             if (mRecentsView != null) {
-                int nearestPage = mRecentsView.getPageNearestToCenterOfScreen();
+                int nearestPage = mRecentsView.getDestinationPage();
                 if (mRecentsView.getNextPage() != nearestPage) {
                     // We shouldn't really scroll to the next page when swiping up to recents.
                     // Only allow settling on the next page if it's nearest to the center.
@@ -971,9 +956,6 @@
                 }
                 duration = Math.max(duration, mRecentsView.getScroller().getDuration());
             }
-            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-                mRecentsView.getRunningTaskView().setIsClickableAsLiveTile(false);
-            }
         }
 
         // Let RecentsView handle the scrolling to the task, which we launch in startNewTask()
@@ -1062,10 +1044,11 @@
         if (mGestureState.getEndTarget().isLauncher) {
             ActivityManagerWrapper.getInstance().registerTaskStackListener(
                     mActivityRestartListener);
+
+            mActivityInterface.onAnimateToLauncher(mGestureState.getEndTarget(), duration);
         }
 
         if (mGestureState.getEndTarget() == HOME) {
-            mTaskViewSimulator.setDrawsBelowRecents(false);
             getOrientationHandler().adjustFloatingIconStartVelocity(velocityPxPerMs);
             final RemoteAnimationTargetCompat runningTaskTarget = mRecentsAnimationTargets != null
                     ? mRecentsAnimationTargets.findTask(mGestureState.getRunningTaskId())
@@ -1075,9 +1058,7 @@
                     && runningTaskTarget != null
                     && runningTaskTarget.pictureInPictureParams != null
                     && TaskInfoCompat.isAutoEnterPipEnabled(
-                            runningTaskTarget.pictureInPictureParams)
-                    && TaskInfoCompat.getPipSourceRectHint(
-                            runningTaskTarget.pictureInPictureParams) != null;
+                            runningTaskTarget.pictureInPictureParams);
             if (mIsSwipingPipToHome) {
                 mSwipePipToHomeAnimator = getSwipePipToHomeAnimator(
                         homeAnimFactory, runningTaskTarget, start);
@@ -1108,8 +1089,8 @@
             homeAnimFactory.playAtomicAnimation(velocityPxPerMs.y);
             mLauncherTransitionController = null;
         } else {
+            AnimatorSet animatorSet = new AnimatorSet();
             ValueAnimator windowAnim = mCurrentShift.animateToValue(start, end);
-            windowAnim.setDuration(duration).setInterpolator(interpolator);
             windowAnim.addUpdateListener(valueAnimator -> {
                 computeRecentsScrollIfInvisible();
             });
@@ -1144,8 +1125,15 @@
                     mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
                 }
             });
-            windowAnim.start();
-            mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
+            animatorSet.play(windowAnim);
+            if (mRecentsView != null && mDp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()
+                    && mGestureState.getEndTarget() == RECENTS) {
+                animatorSet.play(ObjectAnimator.ofFloat(mRecentsView, RECENTS_GRID_PROGRESS, 1));
+                animatorSet.play(mTaskViewSimulator.gridProgress.animateToValue(0, 1));
+            }
+            animatorSet.setDuration(duration).setInterpolator(interpolator);
+            animatorSet.start();
+            mRunningWindowAnim = RunningWindowAnim.wrap(animatorSet);
         }
     }
 
@@ -1180,8 +1168,10 @@
             swipePipToHomeAnimator.setFromRotation(mTaskViewSimulator, windowRotation);
         }
         swipePipToHomeAnimator.addListener(new AnimatorListenerAdapter() {
+            private boolean mHasAnimationEnded;
             @Override
             public void onAnimationStart(Animator animation) {
+                if (mHasAnimationEnded) return;
                 // Ensure Launcher ends in NORMAL state, we intentionally skip other callbacks
                 // since they are not relevant in this swipe-pip-to-home case.
                 homeAnimFactory.createActivityAnimationToHome().dispatchOnStart();
@@ -1189,6 +1179,8 @@
 
             @Override
             public void onAnimationEnd(Animator animation) {
+                if (mHasAnimationEnded) return;
+                mHasAnimationEnded = true;
                 if (mRecentsAnimationController == null) {
                     // If the recents animation is interrupted, we still end the running
                     // animation (not canceled) so this is still called. In that case, we can
@@ -1234,13 +1226,6 @@
         });
         anim.addAnimatorListener(new AnimationSuccessListener() {
             @Override
-            public void onAnimationStart(Animator animation) {
-                if (mActivity != null) {
-                    removeLiveTileOverlay();
-                }
-            }
-
-            @Override
             public void onAnimationSuccess(Animator animator) {
                 if (mRecentsView != null) {
                     mRecentsView.post(mRecentsView::resetTaskVisuals);
@@ -1261,7 +1246,7 @@
             // In the off chance that the gesture ends before Launcher is started, we should clear
             // the callback here so that it doesn't update with the wrong state
             mActivity.clearRunOnceOnStartCallback();
-            resetLauncherListenersAndOverlays();
+            resetLauncherListeners();
         }
         if (mGestureState.getEndTarget() != null && !mGestureState.isRunningAnimationToLauncher()) {
             cancelCurrentAnimation();
@@ -1344,7 +1329,7 @@
         endLauncherTransitionController();
 
         mRecentsView.onGestureAnimationEnd();
-        resetLauncherListenersAndOverlays();
+        resetLauncherListeners();
     }
 
     private void endLauncherTransitionController() {
@@ -1357,13 +1342,12 @@
         }
     }
 
-    private void resetLauncherListenersAndOverlays() {
+    private void resetLauncherListeners() {
         // Reset the callback for deferred activity launches
-        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (!LIVE_TILE.get()) {
             mActivityInterface.setOnDeferredActivityLaunchCallback(null);
         }
         mActivity.getRootView().setOnApplyWindowInsetsListener(null);
-        removeLiveTileOverlay();
     }
 
     private void notifyTransitionCancelled() {
@@ -1380,7 +1364,7 @@
 
     protected void switchToScreenshot() {
         final int runningTaskId = mGestureState.getRunningTaskId();
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             if (mRecentsAnimationController != null) {
                 mRecentsAnimationController.getController().setWillFinishToHome(true);
                 // Update the screenshot of the task
@@ -1449,12 +1433,8 @@
     }
 
     private void finishCurrentTransitionToRecents() {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
-            final TaskView runningTaskView = mRecentsView.getRunningTaskView();
-            if (runningTaskView != null) {
-                runningTaskView.setIsClickableAsLiveTile(true);
-            }
         } else if (!hasTargets() || mRecentsAnimationController == null) {
             // If there are no targets or the animation not started, then there is nothing to finish
             mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
@@ -1500,7 +1480,9 @@
         @Override
         public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
                 boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
-            if (mRecentsAnimationTargets.hasTask(task.taskId)) {
+            if (mRecentsView.getRunningTaskIndex() != -1
+                    && mRecentsView.getRunningTaskId() == task.taskId
+                    && mRecentsAnimationTargets.hasTask(task.taskId)) {
                 launchOtherTaskInLiveTileMode(task.taskId, mRecentsAnimationTargets.apps);
             }
             ActivityManagerWrapper.getInstance().unregisterTaskStackListener(
@@ -1512,7 +1494,7 @@
         endLauncherTransitionController();
         mActivityInterface.onSwipeUpToRecentsComplete();
         mRecentsView.onSwipeUpAnimationSuccess();
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             mTaskAnimationManager.setLaunchOtherTaskInLiveTileModeHandler(
                     appearedTaskTarget -> {
                         RemoteAnimationTargetCompat[] apps = Arrays.copyOf(
@@ -1589,17 +1571,6 @@
         anim.start();
     }
 
-    private void addLiveTileOverlay() {
-        if (LiveTileOverlay.INSTANCE.attach(mActivity.getRootView().getOverlay())) {
-            mRecentsView.setLiveTileOverlayAttached(true);
-        }
-    }
-
-    private void removeLiveTileOverlay() {
-        LiveTileOverlay.INSTANCE.detach(mActivity.getRootView().getOverlay());
-        mRecentsView.setLiveTileOverlayAttached(false);
-    }
-
     private static boolean isNotInRecents(RemoteAnimationTargetCompat app) {
         return app.isNotInRecents
                 || app.activityType == ACTIVITY_TYPE_HOME;
@@ -1752,19 +1723,14 @@
         if (mWindowTransitionController != null) {
             mWindowTransitionController.setProgress(mCurrentShift.value, mDragLengthFactor);
         }
-        if (mRecentsAnimationTargets != null) {
+        // No need to apply any transform if there is ongoing swipe-pip-to-home animator since
+        // that animator handles the leash solely.
+        if (mRecentsAnimationTargets != null && !mIsSwipingPipToHome) {
             if (mRecentsViewScrollLinked) {
                 mTaskViewSimulator.setScroll(mRecentsView.getScrollOffset());
             }
             mTaskViewSimulator.apply(mTransformParams);
         }
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mRecentsAnimationTargets != null) {
-            LiveTileOverlay.INSTANCE.update(
-                    mTaskViewSimulator.getCurrentRect(),
-                    mTaskViewSimulator.getCurrentCornerRadius());
-            LiveTileOverlay.INSTANCE.setRotation(
-                    mRecentsView.getPagedViewOrientedState().getDisplayRotation());
-        }
         ProtoTracer.INSTANCE.get(mContext).scheduleFrameUpdate();
     }
 
diff --git a/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
index efd4530..d159fa0 100644
--- a/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -33,6 +33,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.taskbar.TaskbarController;
 import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.util.SurfaceTransactionApplier;
 import com.android.quickstep.util.TaskViewSimulator;
@@ -119,6 +120,11 @@
                     OVERVIEW.getDepth(mActivity), TOUCH_RESPONSE_INTERPOLATOR);
         }
 
+        TaskbarController taskbarController = mActivityInterface.getTaskbarController();
+        if (taskbarController != null) {
+            pa.add(taskbarController.createAnimToLauncher(OVERVIEW, getRecentsLaunchDuration()));
+        }
+
         RemoteAnimationTargets targets = new RemoteAnimationTargets(appTargets,
                 wallpaperTargets, MODE_CLOSING);
 
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 5bed929..ce14197 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -45,6 +45,7 @@
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.taskbar.TaskbarController;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.WindowBounds;
 import com.android.quickstep.SysUINavigationMode.Mode;
@@ -121,6 +122,11 @@
         return null;
     }
 
+    @Nullable
+    public TaskbarController getTaskbarController() {
+        return null;
+    }
+
     public final boolean isResumed() {
         ACTIVITY_TYPE activity = getCreatedActivity();
         return activity != null && activity.hasBeenResumed();
@@ -147,6 +153,13 @@
         return deviceState.isInDeferredGestureRegion(ev);
     }
 
+    /**
+     * @return Whether the gesture in progress should be cancelled.
+     */
+    public boolean shouldCancelCurrentGesture() {
+        return false;
+    }
+
     public abstract void onExitOverview(RotationTouchHelper deviceState,
             Runnable exitRunnable);
 
@@ -276,6 +289,20 @@
         return overviewActionsHeight;
     }
 
+    /**
+     * Called when the gesture ends and the animation starts towards the given target. No-op by
+     * default, but subclasses can override to add an additional animation with the same duration.
+     */
+    public void onAnimateToLauncher(GestureState.GestureEndTarget endTarget, long duration) {
+    }
+
+    /**
+     * See {@link com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags}
+     * @param systemUiStateFlags The latest SystemUiStateFlags
+     */
+    public void onSystemUiFlagsChanged(int systemUiStateFlags) {
+    }
+
     public interface AnimationFactory {
 
         void createActivityInterface(long transitionLength);
diff --git a/quickstep/src/com/android/quickstep/ImageActionsApi.java b/quickstep/src/com/android/quickstep/ImageActionsApi.java
index cb4d53a..8cb64c2 100644
--- a/quickstep/src/com/android/quickstep/ImageActionsApi.java
+++ b/quickstep/src/com/android/quickstep/ImageActionsApi.java
@@ -64,6 +64,20 @@
      */
     @UiThread
     public void shareWithExplicitIntent(@Nullable Rect crop, Intent intent) {
+        addImageAndSendIntent(crop, intent, false);
+    }
+
+    /**
+     * Share the image this api was constructed with using the provided intent. The implementation
+     * should set the intent's data field to the URI pointing to the image.
+     */
+    @UiThread
+    public void shareAsDataWithExplicitIntent(@Nullable Rect crop, Intent intent) {
+        addImageAndSendIntent(crop, intent, true);
+    }
+
+    @UiThread
+    private void addImageAndSendIntent(@Nullable Rect crop, Intent intent, boolean setData) {
         if (mBitmapSupplier.get() == null) {
             Log.e(TAG, "No snapshot available, not starting share.");
             return;
@@ -71,12 +85,14 @@
 
         UI_HELPER_EXECUTOR.execute(() -> persistBitmapAndStartActivity(mContext,
                 mBitmapSupplier.get(), crop, intent, (uri, intentForUri) -> {
-                    intentForUri
-                            .addFlags(FLAG_GRANT_READ_URI_PERMISSION)
-                            .putExtra(EXTRA_STREAM, uri);
+                    intentForUri.addFlags(FLAG_GRANT_READ_URI_PERMISSION);
+                    if (setData) {
+                        intentForUri.setData(uri);
+                    } else {
+                        intentForUri.putExtra(EXTRA_STREAM, uri);
+                    }
                     return new Intent[]{intentForUri};
                 }, TAG));
-
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 7630bc4..3f3e5ad 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -16,14 +16,17 @@
 package com.android.quickstep;
 
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.quickstep.SysUINavigationMode.getMode;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.util.Log;
+import android.view.MotionEvent;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
@@ -38,8 +41,10 @@
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statehandlers.DepthController.ClampedDepthProperty;
 import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.taskbar.TaskbarController;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.quickstep.GestureState.GestureEndTarget;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.util.ActivityInitListener;
 import com.android.quickstep.util.AnimatorControllerWithResistance;
@@ -156,6 +161,16 @@
 
     @Nullable
     @Override
+    public TaskbarController getTaskbarController() {
+        BaseQuickstepLauncher launcher = getCreatedActivity();
+        if (launcher == null) {
+            return null;
+        }
+        return launcher.getTaskbarController();
+    }
+
+    @Nullable
+    @Override
     public RecentsView getVisibleRecentsView() {
         Launcher launcher = getVisibleLauncher();
         return launcher != null && launcher.getStateManager().getState().overviewUi
@@ -172,9 +187,6 @@
 
     @Override
     public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "switchToRecentsIfVisible");
-        }
         Launcher launcher = getVisibleLauncher();
         if (launcher == null) {
             return false;
@@ -277,4 +289,42 @@
         if (activity == null) return;
         activity.getAppTransitionManager().registerRemoteTransitions();
     }
+
+    @Override
+    public void onAnimateToLauncher(GestureEndTarget endTarget, long duration) {
+        TaskbarController taskbarController = getTaskbarController();
+        if (taskbarController == null) {
+            return;
+        }
+        LauncherState toState = endTarget == GestureEndTarget.RECENTS ? OVERVIEW : NORMAL;
+        taskbarController.createAnimToLauncher(toState, duration).start();
+    }
+
+    @Override
+    public void onSystemUiFlagsChanged(int systemUiStateFlags) {
+        TaskbarController taskbarController = getTaskbarController();
+        if (taskbarController == null) {
+            return;
+        }
+        boolean isImeVisible = (systemUiStateFlags & SYSUI_STATE_IME_SHOWING) != 0;
+        taskbarController.setIsImeVisible(isImeVisible);
+    }
+
+    @Override
+    public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
+        TaskbarController taskbarController = getTaskbarController();
+        if (taskbarController == null) {
+            return super.deferStartingActivity(deviceState, ev);
+        }
+        return taskbarController.isEventOverAnyTaskbarItem(ev);
+    }
+
+    @Override
+    public boolean shouldCancelCurrentGesture() {
+        TaskbarController taskbarController = getTaskbarController();
+        if (taskbarController == null) {
+            return super.shouldCancelCurrentGesture();
+        }
+        return taskbarController.isDraggingItem();
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
index b258a10..a749f9a 100644
--- a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
@@ -30,7 +30,6 @@
 import android.graphics.Point;
 import android.graphics.RectF;
 import android.util.Log;
-import android.util.SparseArray;
 import android.view.MotionEvent;
 import android.view.Surface;
 
@@ -40,6 +39,9 @@
 import com.android.launcher3.util.DisplayController.Info;
 
 import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
 
 /**
  * Maintains state for supporting nav bars and tracking their gestures in multiple orientations.
@@ -51,6 +53,37 @@
  */
 class OrientationTouchTransformer {
 
+    class CurrentDisplay {
+        public Point size;
+        public int rotation;
+
+        CurrentDisplay() {
+            this.size = new Point(0, 0);
+            this.rotation = 0;
+        }
+
+        CurrentDisplay(Point size, int rotation) {
+            this.size = size;
+            this.rotation = rotation;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            CurrentDisplay display = (CurrentDisplay) o;
+            if (rotation != display.rotation) return false;
+
+            return Objects.equals(size, display.size);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(size, rotation);
+        }
+    };
+
     private static final String TAG = "OrientationTouchTransformer";
     private static final boolean DEBUG = false;
     private static final int MAX_ORIENTATIONS = 4;
@@ -60,11 +93,12 @@
     private final Matrix mTmpMatrix = new Matrix();
     private final float[] mTmpPoint = new float[2];
 
-    private SparseArray<OrientationRectF> mSwipeTouchRegions = new SparseArray<>(MAX_ORIENTATIONS);
+    private Map<CurrentDisplay, OrientationRectF> mSwipeTouchRegions =
+            new HashMap<CurrentDisplay, OrientationRectF>();
     private final RectF mAssistantLeftRegion = new RectF();
     private final RectF mAssistantRightRegion = new RectF();
     private final RectF mOneHandedModeRegion = new RectF();
-    private int mCurrentDisplayRotation;
+    private CurrentDisplay mCurrentDisplay = new CurrentDisplay();
     private int mNavBarGesturalHeight;
     private int mNavBarLargerGesturalHeight;
     private boolean mEnableMultipleRegions;
@@ -147,21 +181,22 @@
      * @see #enableMultipleRegions(boolean, Info)
      */
     void createOrAddTouchRegion(Info info) {
-        mCurrentDisplayRotation = info.rotation;
+        mCurrentDisplay = new CurrentDisplay(info.realSize, info.rotation);
+
         if (mQuickStepStartingRotation > QUICKSTEP_ROTATION_UNINITIALIZED
-                && mCurrentDisplayRotation == mQuickStepStartingRotation) {
+                && mCurrentDisplay.rotation == mQuickStepStartingRotation) {
             // User already was swiping and the current screen is same rotation as the starting one
             // Remove active nav bars in other rotations except for the one we started out in
             resetSwipeRegions(info);
             return;
         }
-        OrientationRectF region = mSwipeTouchRegions.get(mCurrentDisplayRotation);
+        OrientationRectF region = mSwipeTouchRegions.get(mCurrentDisplay);
         if (region != null) {
             return;
         }
 
         if (mEnableMultipleRegions) {
-            mSwipeTouchRegions.put(mCurrentDisplayRotation, createRegionForDisplay(info));
+            mSwipeTouchRegions.put(mCurrentDisplay, createRegionForDisplay(info));
         } else {
             resetSwipeRegions(info);
         }
@@ -208,37 +243,36 @@
      */
     private void resetSwipeRegions(Info region) {
         if (DEBUG) {
-            Log.d(TAG, "clearing all regions except rotation: " + mCurrentDisplayRotation);
+            Log.d(TAG, "clearing all regions except rotation: " + mCurrentDisplay.rotation);
         }
 
-        mCurrentDisplayRotation = region.rotation;
-        OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplayRotation);
+        mCurrentDisplay = new CurrentDisplay(region.realSize, region.rotation);
+        OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplay);
         if (regionToKeep == null) {
             regionToKeep = createRegionForDisplay(region);
         }
         mSwipeTouchRegions.clear();
-        mSwipeTouchRegions.put(mCurrentDisplayRotation, regionToKeep);
+        mSwipeTouchRegions.put(mCurrentDisplay, regionToKeep);
         updateAssistantRegions(regionToKeep);
     }
 
     private void resetSwipeRegions() {
-        OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplayRotation);
+        OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplay);
         mSwipeTouchRegions.clear();
         if (regionToKeep != null) {
-            mSwipeTouchRegions.put(mCurrentDisplayRotation, regionToKeep);
+            mSwipeTouchRegions.put(mCurrentDisplay, regionToKeep);
             updateAssistantRegions(regionToKeep);
         }
     }
 
     private OrientationRectF createRegionForDisplay(Info display) {
         if (DEBUG) {
-            Log.d(TAG, "creating rotation region for: " + mCurrentDisplayRotation);
+            Log.d(TAG, "creating rotation region for: " + mCurrentDisplay.rotation);
         }
 
         Point size = display.realSize;
         int rotation = display.rotation;
         int touchHeight = mNavBarGesturalHeight;
-        int largerGesturalHeight = mNavBarLargerGesturalHeight;
         OrientationRectF orientationRectF =
                 new OrientationRectF(0, 0, size.x, size.y, rotation);
         if (mMode == SysUINavigationMode.Mode.NO_BUTTON) {
@@ -341,7 +375,9 @@
                 }
 
                 for (int i = 0; i < MAX_ORIENTATIONS; i++) {
-                    OrientationRectF rect = mSwipeTouchRegions.get(i);
+                    CurrentDisplay display = new CurrentDisplay(mCurrentDisplay.size, i);
+                    OrientationRectF rect = mSwipeTouchRegions.get(display);
+
                     if (TestProtocol.sDebugTracing) {
                         Log.d(TestProtocol.NO_SWIPE_TO_HOME, "transform:DOWN, rect=" + rect);
                     }
@@ -355,7 +391,7 @@
                         mLastRectTouched = rect;
                         mActiveTouchRotation = rect.mRotation;
                         if (mEnableMultipleRegions
-                                && mCurrentDisplayRotation == mActiveTouchRotation) {
+                                && mCurrentDisplay.rotation == mActiveTouchRotation) {
                             // TODO(b/154580671) might make this block unnecessary
                             // Start a touch session for the default nav region for the display
                             mQuickStepStartingRotation = mLastRectTouched.mRotation;
@@ -378,8 +414,8 @@
         pw.println("  lastTouchedRegion=" + mLastRectTouched);
         pw.println("  multipleRegionsEnabled=" + mEnableMultipleRegions);
         StringBuilder regions = new StringBuilder("  currentTouchableRotations=");
-        for(int i = 0; i < mSwipeTouchRegions.size(); i++) {
-            OrientationRectF rectF = mSwipeTouchRegions.get(mSwipeTouchRegions.keyAt(i));
+        for (CurrentDisplay key: mSwipeTouchRegions.keySet()) {
+            OrientationRectF rectF = mSwipeTouchRegions.get(key);
             regions.append(rectF).append(" ");
         }
         pw.println(regions.toString());
@@ -417,12 +453,12 @@
 
         boolean applyTransform(MotionEvent event, boolean forceTransform) {
             mTmpMatrix.reset();
-            postDisplayRotation(deltaRotation(mCurrentDisplayRotation, mRotation),
+            postDisplayRotation(deltaRotation(mCurrentDisplay.rotation, mRotation),
                     mHeight, mWidth, mTmpMatrix);
             if (forceTransform) {
                 if (DEBUG) {
                     Log.d(TAG, "Transforming rotation due to forceTransform, "
-                            + "mCurrentRotation: " + mCurrentDisplayRotation
+                            + "mCurrentRotation: " + mCurrentDisplay.rotation
                             + "mRotation: " + mRotation);
                 }
                 event.transform(mTmpMatrix);
diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
index 65847f1..192738f 100644
--- a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
+++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
@@ -15,6 +15,8 @@
  */
 package com.android.quickstep;
 
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
+
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -59,6 +61,8 @@
 
         super.init(context);
 
+        LIVE_TILE.initialize(context);
+
         // Elevate GPU priority for Quickstep and Remote animations.
         ThreadedRendererCompat.setContextPriority(
                 ThreadedRendererCompat.EGL_CONTEXT_PRIORITY_HIGH_IMG);
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 70b4f20..2f1538b 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -22,10 +22,12 @@
 import android.app.ActivityManager;
 import android.os.Build;
 import android.os.Process;
+import android.util.Log;
 import android.util.SparseBooleanArray;
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -51,6 +53,9 @@
 
     // The list change id, increments as the task list changes in the system
     private int mChangeId;
+    // Whether we are currently updating the tasks in the background (up to when the result is
+    // posted back on the main thread)
+    private boolean mLoadingTasksInBackground;
 
     private TaskLoadResult mResultsBg = INVALID_RESULT;
     private TaskLoadResult mResultsUi = INVALID_RESULT;
@@ -64,6 +69,11 @@
         mActivityManagerWrapper.registerTaskStackListener(this);
     }
 
+    @VisibleForTesting
+    public boolean isLoadingTasksInBackground() {
+        return mLoadingTasksInBackground;
+    }
+
     /**
      * Fetches the task keys skipping any local cache.
      */
@@ -83,6 +93,10 @@
      * @return The change id of the current task list
      */
     public synchronized int getTasks(boolean loadKeysOnly, Consumer<ArrayList<Task>> callback) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.GET_RECENTS_FAILED, "getTasks: keysOnly=" + loadKeysOnly
+                    + " callback=" + callback);
+        }
         final int requestLoadId = mChangeId;
         if (mResultsUi.isValidForRequest(requestLoadId, loadKeysOnly)) {
             // The list is up to date, send the callback on the next frame,
@@ -90,22 +104,38 @@
             if (callback != null) {
                 // Copy synchronously as the changeId might change by next frame
                 ArrayList<Task> result = copyOf(mResultsUi);
-                mMainThreadExecutor.post(() -> callback.accept(result));
+                mMainThreadExecutor.post(() -> {
+                    if (TestProtocol.sDebugTracing) {
+                        Log.d(TestProtocol.GET_RECENTS_FAILED, "getTasks: no new tasks");
+                    }
+                    callback.accept(result);
+                });
             }
 
             return requestLoadId;
         }
 
         // Kick off task loading in the background
+        mLoadingTasksInBackground = true;
         UI_HELPER_EXECUTOR.execute(() -> {
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.GET_RECENTS_FAILED, "getTasks: loading in bg start");
+            }
             if (!mResultsBg.isValidForRequest(requestLoadId, loadKeysOnly)) {
                 mResultsBg = loadTasksInBackground(Integer.MAX_VALUE, requestLoadId, loadKeysOnly);
             }
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.GET_RECENTS_FAILED, "getTasks: loading in bg end");
+            }
             TaskLoadResult loadResult = mResultsBg;
             mMainThreadExecutor.execute(() -> {
+                mLoadingTasksInBackground = false;
                 mResultsUi = loadResult;
                 if (callback != null) {
                     ArrayList<Task> result = copyOf(mResultsUi);
+                    if (TestProtocol.sDebugTracing) {
+                        Log.d(TestProtocol.GET_RECENTS_FAILED, "getTasks: callback w/ bg results");
+                    }
                     callback.accept(result);
                 }
             });
@@ -191,6 +221,7 @@
             } else {
                 task = new Task(taskKey);
             }
+            task.setLastSnapshotData(rawTask);
             allTasks.add(task);
         }
 
@@ -200,9 +231,7 @@
     private ArrayList<Task> copyOf(ArrayList<Task> tasks) {
         ArrayList<Task> newTasks = new ArrayList<>();
         for (int i = 0; i < tasks.size(); i++) {
-            Task t = tasks.get(i);
-            newTasks.add(new Task(t.key, t.colorPrimary, t.colorBackground, t.isDockable,
-                    t.isLocked, t.taskDescription, t.topActivity));
+            newTasks.add(new Task(tasks.get(i)));
         }
         return newTasks;
     }
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 7beeae2..8aa0842 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -184,8 +184,11 @@
             }
 
             @Override
-            public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
-                    RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result) {
+            public void onCreateAnimation(int transit,
+                    RemoteAnimationTargetCompat[] appTargets,
+                    RemoteAnimationTargetCompat[] wallpaperTargets,
+                    RemoteAnimationTargetCompat[] nonAppTargets,
+                    AnimationResult result) {
                 AnimatorSet anim = composeRecentsLaunchAnimator(taskView, appTargets,
                         wallpaperTargets);
                 anim.addListener(resetStateListener());
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 4301377..f99b7e6 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -17,14 +17,16 @@
 
 import static android.content.Intent.ACTION_USER_UNLOCKED;
 
+import static com.android.launcher3.util.SettingsCache.ONE_HANDED_ENABLED;
+import static com.android.launcher3.util.SettingsCache.ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED;
 import static com.android.launcher3.util.DisplayController.DisplayHolder.CHANGE_ALL;
 import static com.android.launcher3.util.DisplayController.DisplayHolder.CHANGE_FRAME_DELAY;
 import static com.android.quickstep.SysUINavigationMode.Mode.NO_BUTTON;
 import static com.android.quickstep.SysUINavigationMode.Mode.THREE_BUTTONS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
@@ -44,6 +46,7 @@
 import android.content.IntentFilter;
 import android.content.res.Resources;
 import android.graphics.Region;
+import android.net.Uri;
 import android.os.Process;
 import android.os.SystemProperties;
 import android.os.UserManager;
@@ -57,11 +60,11 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.DisplayController.DisplayHolder;
 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener;
 import com.android.launcher3.util.DisplayController.Info;
-import com.android.launcher3.util.SecureSettingsObserver;
 import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
 import com.android.quickstep.SysUINavigationMode.OneHandedModeChangeListener;
 import com.android.quickstep.util.NavBarPosition;
@@ -177,33 +180,33 @@
             }
         }
 
+        SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext);
         if (mIsOneHandedModeSupported) {
-            SecureSettingsObserver oneHandedEnabledObserver =
-                    SecureSettingsObserver.newOneHandedSettingsObserver(
-                            mContext, enabled -> mIsOneHandedModeEnabled = enabled);
-            oneHandedEnabledObserver.register();
-            oneHandedEnabledObserver.dispatchOnChange();
-            runOnDestroy(oneHandedEnabledObserver::unregister);
+            Uri oneHandedUri = Settings.Secure.getUriFor(ONE_HANDED_ENABLED);
+            SettingsCache.OnChangeListener onChangeListener =
+                    enabled -> mIsOneHandedModeEnabled = enabled;
+            settingsCache.register(oneHandedUri, onChangeListener);
+            settingsCache.dispatchOnChange(oneHandedUri);
+            runOnDestroy(() -> settingsCache.unregister(oneHandedUri, onChangeListener));
         } else {
             mIsOneHandedModeEnabled = false;
         }
 
-        SecureSettingsObserver swipeBottomEnabledObserver =
-                SecureSettingsObserver.newSwipeToNotificationSettingsObserver(
-                        mContext, enabled -> mIsSwipeToNotificationEnabled = enabled);
-        swipeBottomEnabledObserver.register();
-        swipeBottomEnabledObserver.dispatchOnChange();
-        runOnDestroy(swipeBottomEnabledObserver::unregister);
 
-        SecureSettingsObserver userSetupObserver = new SecureSettingsObserver(
-                context.getContentResolver(),
-                e -> mIsUserSetupComplete = e,
-                Settings.Secure.USER_SETUP_COMPLETE,
-                0);
-        mIsUserSetupComplete = userSetupObserver.getValue();
+        Uri swipeBottomNotificationUri =
+                Settings.Secure.getUriFor(ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED);
+        SettingsCache.OnChangeListener onChangeListener =
+                enabled -> mIsSwipeToNotificationEnabled = enabled;
+        settingsCache.register(swipeBottomNotificationUri, onChangeListener);
+        settingsCache.dispatchOnChange(swipeBottomNotificationUri);
+        runOnDestroy(() -> settingsCache.unregister(swipeBottomNotificationUri, onChangeListener));
+
+        Uri setupCompleteUri = Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE);
+        mIsUserSetupComplete = settingsCache.getValue(setupCompleteUri, 0);
         if (!mIsUserSetupComplete) {
-            userSetupObserver.register();
-            runOnDestroy(userSetupObserver::unregister);
+            SettingsCache.OnChangeListener userSetupChangeListener = e -> mIsUserSetupComplete = e;
+            settingsCache.register(setupCompleteUri, userSetupChangeListener);
+            runOnDestroy(() -> settingsCache.unregister(setupCompleteUri, userSetupChangeListener));
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index d47217b..ba24e6a 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -28,6 +28,8 @@
 import android.os.Process;
 import android.os.UserHandle;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.util.Executors.SimpleThreadFactory;
 import com.android.launcher3.util.MainThreadInitializedObject;
@@ -102,6 +104,14 @@
     }
 
     /**
+     * @return Whether the task list is currently updating in the background
+     */
+    @VisibleForTesting
+    public boolean isLoadingTasksInBackground() {
+        return mTaskList.isLoadingTasksInBackground();
+    }
+
+    /**
      * Finds and returns the task key associated with the given task id.
      *
      * @param callback The callback to receive the task key if it is found or null. This is always
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index ca73041..f4b8b62 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -94,13 +94,7 @@
         mTransitionDragLength = mGestureState.getActivityInterface().getSwipeUpDestinationAndLength(
                 dp, mContext, TEMP_RECT,
                 mTaskViewSimulator.getOrientationState().getOrientationHandler());
-
-        if (mDeviceState.isFullyGesturalNavMode()) {
-            // We can drag all the way to the top of the screen.
-            mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
-        } else {
-            mDragLengthFactor = 1 + AnimatorControllerWithResistance.TWO_BUTTON_EXTRA_DRAG_FACTOR;
-        }
+        mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
 
         PendingAnimation pa = new PendingAnimation(mTransitionDragLength * 2);
         mTaskViewSimulator.addAppToOverviewAnim(pa, LINEAR);
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index ca55468..5668817 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -17,9 +17,11 @@
 
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
+import android.app.PendingIntent;
 import android.app.PictureInPictureParams;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.graphics.Bitmap;
 import android.graphics.Insets;
@@ -28,11 +30,14 @@
 import android.os.IBinder;
 import android.os.IBinder.DeathRecipient;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.util.Log;
 import android.view.MotionEvent;
 
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.systemui.shared.recents.IPinnedStackAnimationListener;
+import com.android.systemui.shared.recents.ISplitScreenListener;
+import com.android.systemui.shared.recents.IStartingWindowListener;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.RemoteTransitionCompat;
@@ -431,4 +436,121 @@
             }
         }
     }
+
+    @Override
+    public void registerSplitScreenListener(ISplitScreenListener listener) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.registerSplitScreenListener(listener);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call registerSplitScreenListener");
+            }
+        }
+    }
+
+    @Override
+    public void unregisterSplitScreenListener(ISplitScreenListener listener) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.unregisterSplitScreenListener(listener);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call unregisterSplitScreenListener");
+            }
+        }
+    }
+
+    @Override
+    public void setSideStageVisibility(boolean visible) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.setSideStageVisibility(visible);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call setSideStageVisibility");
+            }
+        }
+    }
+
+    @Override
+    public void exitSplitScreen() {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.exitSplitScreen();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call exitSplitScreen");
+            }
+        }
+    }
+
+    @Override
+    public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.exitSplitScreenOnHide(exitSplitScreenOnHide);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call exitSplitScreen");
+            }
+        }
+    }
+
+    @Override
+    public void startTask(int taskId, int stage, int position, Bundle options) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.startTask(taskId, stage, position, options);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call startTask");
+            }
+        }
+    }
+
+    @Override
+    public void startShortcut(String packageName, String shortcutId, int stage, int position,
+            Bundle options, UserHandle user) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.startShortcut(packageName, shortcutId, stage, position, options,
+                        user);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call startShortcut");
+            }
+        }
+    }
+
+    @Override
+    public void startIntent(PendingIntent intent, Intent fillInIntent, int stage,
+            int position, Bundle options) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.startIntent(intent, fillInIntent, stage, position,
+                        options);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call startIntent");
+            }
+        }
+    }
+
+    @Override
+    public void removeFromSideStage(int taskId) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.removeFromSideStage(taskId);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call removeFromSideStage");
+            }
+        }
+    }
+
+    /**
+     * Sets listener to get callbacks when launching a task.
+     */
+    @Override
+    public void setStartingWindowListener(IStartingWindowListener listener) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.setStartingWindowListener(listener);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call setStartingWindowListener", e);
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index 93ebd5a..8636130 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -18,7 +18,7 @@
 
 import static android.view.Surface.ROTATION_0;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL;
 import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
 
@@ -122,12 +122,11 @@
      */
     public static class TaskOverlay<T extends OverviewActionsView> {
 
-        private final Context mApplicationContext;
+        protected final Context mApplicationContext;
         protected final TaskThumbnailView mThumbnailView;
 
         private T mActionsView;
-        private ImageActionsApi mImageApi;
-        private boolean mIsAllowedByPolicy;
+        protected ImageActionsApi mImageApi;
 
         protected TaskOverlay(TaskThumbnailView taskThumbnailView) {
             mApplicationContext = taskThumbnailView.getContext().getApplicationContext();
@@ -153,24 +152,8 @@
 
             if (thumbnail != null) {
                 getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
-                final boolean isAllowedByPolicy = thumbnail.isRealSnapshot;
-
-                getActionsView().setCallbacks(new OverlayUICallbacks() {
-                    @Override
-                    public void onShare() {
-                        if (isAllowedByPolicy) {
-                            endLiveTileMode(() -> mImageApi.startShareActivity(null));
-                        } else {
-                            showBlockedByPolicyMessage();
-                        }
-                    }
-
-                    @SuppressLint("NewApi")
-                    @Override
-                    public void onScreenshot() {
-                        endLiveTileMode(() -> saveScreenshot(task));
-                    }
-                });
+                boolean isAllowedByPolicy = thumbnail.isRealSnapshot;
+                getActionsView().setCallbacks(new OverlayUICallbacksImpl(isAllowedByPolicy, task));
             }
         }
 
@@ -180,7 +163,7 @@
          * @param callback callback to run, after switching to screenshot
          */
         public void endLiveTileMode(@NonNull Runnable callback) {
-            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            if (LIVE_TILE.get()) {
                 RecentsView recentsView = mThumbnailView.getTaskView().getRecentsView();
                 recentsView.switchToScreenshot(
                         () -> recentsView.finishRecentsAnimation(true /* toRecents */, callback));
@@ -193,7 +176,7 @@
          * Called to save screenshot of the task thumbnail.
          */
         @SuppressLint("NewApi")
-        private void saveScreenshot(Task task) {
+        protected void saveScreenshot(Task task) {
             if (mThumbnailView.isRealSnapshot()) {
                 mImageApi.saveScreenshot(mThumbnailView.getThumbnail(),
                         getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key);
@@ -222,6 +205,12 @@
         }
 
         /**
+         * Sets full screen progress to the task overlay.
+         */
+        public void setFullscreenProgress(float progress) {
+        }
+
+        /**
          * Gets the system shortcut for the screenshot that will be added to the task menu.
          */
         public SystemShortcut getScreenshotShortcut(BaseDraggingActivity activity,
@@ -251,7 +240,7 @@
             return mThumbnailView.getScaledInsets();
         }
 
-        private void showBlockedByPolicyMessage() {
+        protected void showBlockedByPolicyMessage() {
             Toast.makeText(
                     mThumbnailView.getContext(),
                     R.string.blocked_by_policy,
@@ -273,6 +262,29 @@
                 dismissTaskMenuView(mActivity);
             }
         }
+
+        protected class OverlayUICallbacksImpl implements OverlayUICallbacks {
+            protected final boolean mIsAllowedByPolicy;
+            protected final Task mTask;
+
+            public OverlayUICallbacksImpl(boolean isAllowedByPolicy, Task task) {
+                mIsAllowedByPolicy = isAllowedByPolicy;
+                mTask = task;
+            }
+
+            public void onShare() {
+                if (mIsAllowedByPolicy) {
+                    endLiveTileMode(() -> mImageApi.startShareActivity(null));
+                } else {
+                    showBlockedByPolicyMessage();
+                }
+            }
+
+            @SuppressLint("NewApi")
+            public void onScreenshot() {
+                endLiveTileMode(() -> saveScreenshot(mTask));
+            }
+        }
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 9da306e..17822e6 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -23,14 +23,15 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
 import static com.android.launcher3.anim.Interpolators.clampToProgress;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.statehandlers.DepthController.DEPTH;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 import android.annotation.TargetApi;
 import android.content.ComponentName;
 import android.content.Context;
@@ -49,6 +50,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StateManager;
@@ -137,7 +139,7 @@
         boolean isRunningTask = v.isRunningTask();
         TransformParams params = null;
         TaskViewSimulator tsv = null;
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask) {
+        if (LIVE_TILE.get() && isRunningTask) {
             params = v.getRecentsView().getLiveTileParams();
             tsv = v.getRecentsView().getLiveTileTaskViewSimulator();
         }
@@ -157,8 +159,7 @@
         boolean isQuickSwitch = v.isEndQuickswitchCuj();
         v.setEndQuickswitchCuj(false);
 
-        boolean inLiveTileMode =
-                ENABLE_QUICKSTEP_LIVE_TILE.get() && v.getRecentsView().getRunningTaskIndex() != -1;
+        boolean inLiveTileMode = LIVE_TILE.get() && v.getRecentsView().getRunningTaskIndex() != -1;
         final RemoteAnimationTargets targets =
                 new RemoteAnimationTargets(appTargets, wallpaperTargets,
                         inLiveTileMode ? MODE_CLOSING : MODE_OPENING);
@@ -174,11 +175,14 @@
 
         final RecentsView recentsView = v.getRecentsView();
         int taskIndex = recentsView.indexOfChild(v);
-        boolean parallaxCenterAndAdjacentTask = taskIndex != recentsView.getCurrentPage();
-        int startScroll = recentsView.getScrollOffset(taskIndex);
-
         Context context = v.getContext();
         DeviceProfile dp = BaseActivity.fromContext(context).getDeviceProfile();
+        boolean parallaxCenterAndAdjacentTask =
+                taskIndex != recentsView.getCurrentPage() && !(dp.isTablet
+                        && FeatureFlags.ENABLE_OVERVIEW_GRID.get());
+        float gridTranslationSecondary = recentsView.getGridTranslationSecondary(taskIndex);
+        int startScroll = recentsView.getScrollOffset(taskIndex);
+
         TaskViewSimulator topMostSimulator = null;
 
         if (tsv == null && targets.apps.length > 0) {
@@ -193,6 +197,8 @@
             tsv.setPreview(targets.apps[targets.apps.length - 1]);
             tsv.fullScreenProgress.value = 0;
             tsv.recentsViewScale.value = 1;
+            tsv.gridProgress.value = 1;
+            tsv.gridTranslationSecondary.value = gridTranslationSecondary;
             tsv.setScroll(startScroll);
 
             // Fade in the task during the initial 20% of the animation
@@ -205,6 +211,7 @@
                     AnimatedFloat.VALUE, 1, TOUCH_RESPONSE_INTERPOLATOR);
             out.setFloat(tsv.recentsViewScale,
                     AnimatedFloat.VALUE, tsv.getFullScreenScale(), TOUCH_RESPONSE_INTERPOLATOR);
+            out.setFloat(tsv.gridProgress, AnimatedFloat.VALUE, 0, TOUCH_RESPONSE_INTERPOLATOR);
             out.setInt(tsv, TaskViewSimulator.SCROLL, 0, TOUCH_RESPONSE_INTERPOLATOR);
 
             TaskViewSimulator finalTsv = tsv;
@@ -304,7 +311,11 @@
         Animator launcherAnim;
         final AnimatorListenerAdapter windowAnimEndListener;
         if (launcherClosing) {
-            launcherAnim = recentsView.createAdjacentPageAnimForTaskLaunch(taskView);
+            Context context = v.getContext();
+            DeviceProfile dp = BaseActivity.fromContext(context).getDeviceProfile();
+            launcherAnim = dp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()
+                    ? ObjectAnimator.ofFloat(recentsView, RecentsView.CONTENT_ALPHA, 0)
+                    : recentsView.createAdjacentPageAnimForTaskLaunch(taskView);
             launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
             launcherAnim.setDuration(RECENTS_LAUNCH_DURATION);
 
@@ -333,7 +344,7 @@
             };
         }
         pa.add(launcherAnim);
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && recentsView.getRunningTaskIndex() != -1) {
+        if (LIVE_TILE.get() && recentsView.getRunningTaskIndex() != -1) {
             pa.addOnFrameCallback(recentsView::redrawLiveTile);
         }
         anim.play(pa.buildAnim());
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 0f40937..fc805d0 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -21,10 +21,9 @@
 import static android.view.MotionEvent.ACTION_UP;
 
 import static com.android.launcher3.config.FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.GestureState.DEFAULT_STATE;
-import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
 
@@ -38,18 +37,23 @@
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.graphics.drawable.Icon;
+import android.hardware.display.DisplayManager;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.SystemClock;
+import android.os.SystemProperties;
 import android.util.Log;
 import android.view.Choreographer;
+import android.view.Display;
 import android.view.InputEvent;
 import android.view.MotionEvent;
+import android.view.Surface;
 import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.BinderThread;
@@ -92,6 +96,7 @@
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.InputChannelCompat;
 import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver;
 import com.android.systemui.shared.system.InputConsumerController;
 import com.android.systemui.shared.system.InputMonitorCompat;
@@ -122,6 +127,9 @@
      */
     private static final int SYSTEM_ACTION_ID_ALL_APPS = 14;
 
+    public static final boolean ENABLE_PER_WINDOW_INPUT_ROTATION =
+            SystemProperties.getBoolean("persist.debug.per_window_input_rotation", false);
+
     private int mBackGestureNotificationCounter = -1;
     @Nullable
     private OverscrollPlugin mOverscrollPlugin;
@@ -249,6 +257,8 @@
     private InputMonitorCompat mInputMonitorCompat;
     private InputEventReceiver mInputEventReceiver;
 
+    private DisplayManager mDisplayManager;
+
     @Override
     public void onCreate() {
         super.onCreate();
@@ -262,6 +272,7 @@
         mDeviceState.addOneHandedModeChangedCallback(this::onOneHandedModeOverlayChanged);
         mDeviceState.runOnUserUnlocked(this::onUserUnlocked);
         ProtoTracer.INSTANCE.get(this).add(this);
+        mDisplayManager = getSystemService(DisplayManager.class);
 
         sConnected = true;
     }
@@ -279,13 +290,17 @@
 
     private void initInputMonitor() {
         disposeEventHandlers();
-        if (mDeviceState.isButtonNavMode() || !SystemUiProxy.INSTANCE.get(this).isActive()) {
+
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.TIS_NO_EVENTS, "initInputMonitor: isButtonMode? "
+                    + mDeviceState.isButtonNavMode());
+        }
+
+        if (mDeviceState.isButtonNavMode()) {
             return;
         }
 
-        Bundle bundle = SystemUiProxy.INSTANCE.get(this).monitorGestureInput("swipe-up",
-                mDeviceState.getDisplayId());
-        mInputMonitorCompat = InputMonitorCompat.fromBundle(bundle, KEY_EXTRA_INPUT_MONITOR);
+        mInputMonitorCompat = new InputMonitorCompat("swipe-up", mDeviceState.getDisplayId());
         mInputEventReceiver = mInputMonitorCompat.getInputReceiver(Looper.getMainLooper(),
                 mMainChoreographer, this::onInputEvent);
 
@@ -370,12 +385,14 @@
     @UiThread
     private void onSystemUiFlagsChanged() {
         if (mDeviceState.isUserUnlocked()) {
-            SystemUiProxy.INSTANCE.get(this).setLastSystemUiStateFlags(
-                    mDeviceState.getSystemUiStateFlags());
+            int systemUiStateFlags = mDeviceState.getSystemUiStateFlags();
+            SystemUiProxy.INSTANCE.get(this).setLastSystemUiStateFlags(systemUiStateFlags);
             mOverviewComponentObserver.onSystemUiStateChanged();
+            mOverviewComponentObserver.getActivityInterface().onSystemUiFlagsChanged(
+                    systemUiStateFlags);
 
             // Update the tracing state
-            if ((mDeviceState.getSystemUiStateFlags() & SYSUI_STATE_TRACING_ENABLED) != 0) {
+            if ((systemUiStateFlags & SYSUI_STATE_TRACING_ENABLED) != 0) {
                 Log.d(TAG, "Starting tracing.");
                 ProtoTracer.INSTANCE.get(this).start();
             } else {
@@ -427,6 +444,15 @@
             return;
         }
         MotionEvent event = (MotionEvent) ev;
+        if (ENABLE_PER_WINDOW_INPUT_ROTATION) {
+            final Display display = mDisplayManager.getDisplay(mDeviceState.getDisplayId());
+            int rotation = display.getRotation();
+            Point sz = new Point();
+            display.getRealSize(sz);
+            if (rotation != Surface.ROTATION_0) {
+                event.transform(InputChannelCompat.createRotationMatrix(rotation, sz.x, sz.y));
+            }
+        }
 
         TestLogging.recordMotionEvent(
                 TestProtocol.SEQUENCE_TIS, "TouchInteractionService.onInputEvent", event);
@@ -512,9 +538,14 @@
             }
         }
 
-        boolean cleanUpConsumer = (action == ACTION_UP || action == ACTION_CANCEL)
+        boolean cancelGesture = mGestureState.getActivityInterface() != null
+                && mGestureState.getActivityInterface().shouldCancelCurrentGesture();
+        boolean cleanUpConsumer = (action == ACTION_UP || action == ACTION_CANCEL || cancelGesture)
                 && mConsumer != null
                 && !mConsumer.getActiveConsumerInHierarchy().isConsumerDetachedFromGesture();
+        if (cancelGesture) {
+            event.setAction(ACTION_CANCEL);
+        }
         mUncheckedConsumer.onMotionEvent(event);
 
         if (cleanUpConsumer) {
@@ -656,8 +687,7 @@
                     runningComponent != null && runningComponent.equals(homeComponent);
         }
 
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()
-                && gestureState.getActivityInterface().isInLiveTileMode()) {
+        if (LIVE_TILE.get() && gestureState.getActivityInterface().isInLiveTileMode()) {
             return createOverviewInputConsumer(
                     previousGestureState, gestureState, event, forceOverviewInputConsumer);
         } else if (gestureState.getRunningTask() == null) {
@@ -713,8 +743,7 @@
                 || previousGestureState.isRunningAnimationToLauncher()
                 || (ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
                     && forceOverviewInputConsumer)
-                || (ENABLE_QUICKSTEP_LIVE_TILE.get())
-                    && gestureState.getActivityInterface().isInLiveTileMode()) {
+                || (LIVE_TILE.get()) && gestureState.getActivityInterface().isInLiveTileMode()) {
             return new OverviewInputConsumer(gestureState, activity, mInputMonitorCompat,
                     false /* startingInActivityBounds */);
         } else {
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 8f2356c..13f6137 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -24,9 +24,11 @@
 import android.content.Context;
 import android.os.Build;
 import android.util.AttributeSet;
+import android.util.Log;
 
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.quickstep.FallbackActivityInterface;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.RecentsActivity;
@@ -120,6 +122,10 @@
         // as well. This tile is never shown as we have setCurrentTaskHidden, but allows use to
         // track the index of the next task appropriately, as if we are switching on any other app.
         if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == mRunningTaskId && !tasks.isEmpty()) {
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.GET_RECENTS_FAILED,
+                        "FallbackRecentsView.applyLoadPlan: running task is home");
+            }
             // Check if the task list has running task
             boolean found = false;
             for (Task t : tasks) {
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
index b10bdde..cd69cf1 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
@@ -21,14 +21,18 @@
 import static android.view.MotionEvent.ACTION_MOVE;
 import static android.view.MotionEvent.ACTION_UP;
 
+import static com.android.launcher3.ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE;
 import static com.android.launcher3.Utilities.squaredHypot;
 
 import android.content.Context;
+import android.graphics.Point;
 import android.graphics.PointF;
 import android.view.MotionEvent;
 
 import com.android.launcher3.R;
+import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.InputConsumer;
 import com.android.quickstep.RecentsAnimationDeviceState;
 import com.android.quickstep.SystemUiProxy;
@@ -45,11 +49,15 @@
     private static final int ANGLE_MIN = 30;
 
     private final Context mContext;
+    private final DisplayController.DisplayHolder mDisplayHolder;
+    private final Point mDisplaySize;
     private final RecentsAnimationDeviceState mDeviceState;
 
     private final float mDragDistThreshold;
     private final float mSquaredSlop;
 
+    private final int mNavBarSize;
+
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
 
@@ -60,10 +68,14 @@
             InputConsumer delegate, InputMonitorCompat inputMonitor) {
         super(delegate, inputMonitor);
         mContext = context;
+        mDisplayHolder = DisplayController.getDefaultDisplay(mContext);
         mDeviceState = deviceState;
         mDragDistThreshold = context.getResources().getDimensionPixelSize(
                 R.dimen.gestures_onehanded_drag_threshold);
         mSquaredSlop = Utilities.squaredTouchSlop(context);
+        mDisplaySize = mDisplayHolder.getInfo().realSize;
+        mNavBarSize = ResourceUtils.getNavbarSize(NAVBAR_BOTTOM_GESTURE_SIZE,
+                mContext.getResources());
     }
 
     @Override
@@ -96,7 +108,8 @@
                                 mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y))
                                 || (mDeviceState.isOneHandedModeActive() && isValidExitAngle(
                                 mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y))) {
-                            mPassedSlop = true;
+                            // To avoid mis-trigger when motion not touch system gesture region.
+                            mPassedSlop = isInSystemGestureRegion(mLastPos);
                             setActive(ev);
                         } else {
                             mState = STATE_DELEGATE_ACTIVE;
@@ -154,6 +167,11 @@
         SystemUiProxy.INSTANCE.get(mContext).stopOneHandedMode();
     }
 
+    private boolean isInSystemGestureRegion(PointF lastPos) {
+        final int navBarUpperBound = mDisplaySize.y - mNavBarSize;
+        return mDeviceState.isGesturalNavMode() && lastPos.y > navBarUpperBound;
+    }
+
     private boolean isValidStartAngle(float deltaX, float deltaY) {
         final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
         return angle > -(ANGLE_MAX) && angle < -(ANGLE_MIN);
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index f2f9fb7..5baf518 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -38,10 +38,12 @@
 import android.content.ContextWrapper;
 import android.content.Intent;
 import android.graphics.PointF;
+import android.hardware.display.DisplayManager;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.Log;
+import android.view.Display;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
@@ -64,6 +66,7 @@
 import com.android.quickstep.RotationTouchHelper;
 import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.TaskUtils;
+import com.android.quickstep.TouchInteractionService;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.CachedEventDispatcher;
 import com.android.quickstep.util.MotionPauseDetector;
@@ -113,6 +116,8 @@
     private final PointF mLastPos = new PointF();
     private int mActivePointerId = INVALID_POINTER_ID;
 
+    private int mLastRotation = -1;
+
     // Distance after which we start dragging the window.
     private final float mTouchSlop;
 
@@ -130,6 +135,8 @@
     // Might be displacement in X or Y, depending on the direction we are swiping from the nav bar.
     private float mStartDisplacement;
 
+    private final DisplayManager mDisplayManager;
+
     private Handler mMainThreadHandler;
     private Runnable mCancelRecentsAnimationRunnable = () -> {
         ActivityManagerWrapper.getInstance().cancelRecentsAnimation(
@@ -172,6 +179,7 @@
         mPassedPilferInputSlop = mPassedWindowMoveSlop = continuingPreviousGesture;
         mDisableHorizontalSwipe = !mPassedPilferInputSlop && disableHorizontalSwipe;
         mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
+        mDisplayManager = getSystemService(DisplayManager.class);
     }
 
     @Override
@@ -197,6 +205,17 @@
             return;
         }
 
+        if (TouchInteractionService.ENABLE_PER_WINDOW_INPUT_ROTATION) {
+            final Display display = mDisplayManager.getDisplay(mDeviceState.getDisplayId());
+            final int rotation = display.getRotation();
+            if (rotation != mLastRotation) {
+                // If rotation changes, reset tracking to avoid degenerate velocities.
+                mLastPos.set(ev.getX(), ev.getY());
+                mVelocityTracker.clear();
+                mLastRotation = rotation;
+            }
+        }
+
         // Proxy events to recents view
         if (mPassedWindowMoveSlop && mInteractionHandler != null
                 && !mRecentsViewDispatcher.hasConsumer()) {
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
index aad70c4..cee3363 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
@@ -15,7 +15,7 @@
  */
 package com.android.quickstep.inputconsumers;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.view.KeyEvent;
@@ -99,7 +99,7 @@
 
     @Override
     public void onKeyEvent(KeyEvent ev) {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             mActivity.dispatchKeyEvent(ev);
         }
     }
diff --git a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
index 3157865..f336bf5 100644
--- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
+++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
@@ -28,7 +28,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_DISABLED;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_DOT_ENABLED;
 import static com.android.launcher3.model.QuickstepModelDelegate.LAST_PREDICTION_ENABLED_STATE;
-import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
+import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
 
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -43,7 +43,7 @@
 import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
-import com.android.launcher3.util.SecureSettingsObserver;
+import com.android.launcher3.util.SettingsCache;
 import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.SysUINavigationMode.Mode;
 import com.android.quickstep.SysUINavigationMode.NavigationModeChangeListener;
@@ -77,11 +77,10 @@
         getPrefs(context).registerOnSharedPreferenceChangeListener(this);
         getDevicePrefs(context).registerOnSharedPreferenceChangeListener(this);
 
-        SecureSettingsObserver dotsObserver =
-                newNotificationSettingsObserver(context, this::onNotificationDotsChanged);
-        mNotificationDotsEnabled = dotsObserver.getValue();
-        dispatchUserEvent();
-
+        SettingsCache mSettingsCache = SettingsCache.INSTANCE.get(context);
+        mSettingsCache.register(NOTIFICATION_BADGING_URI,
+                this::onNotificationDotsChanged);
+        mSettingsCache.dispatchOnChange(NOTIFICATION_BADGING_URI);
     }
 
     private static ArrayMap<String, LoggablePref> loadPrefKeys(Context context) {
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index d949126..66c24c8 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -16,8 +16,13 @@
 
 package com.android.quickstep.logging;
 
+import static androidx.core.util.Preconditions.checkNotNull;
+import static androidx.core.util.Preconditions.checkState;
+
+import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.EXTENDED_CONTAINERS;
 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.FOLDER;
 import static com.android.launcher3.logger.LauncherAtom.ContainerInfo.ContainerCase.SEARCH_RESULT_CONTAINER;
+import static com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers.ContainerCase.DEVICE_SEARCH_RESULT_CONTAINER;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WORKSPACE_SNAPSHOT;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__ALLAPPS;
 import static com.android.systemui.shared.system.SysUiStatsLog.LAUNCHER_UICHANGED__DST_STATE__BACKGROUND;
@@ -26,8 +31,11 @@
 
 import android.content.Context;
 import android.util.Log;
+import android.view.View;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.WorkerThread;
+import androidx.slice.SliceItem;
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.Utilities;
@@ -37,6 +45,8 @@
 import com.android.launcher3.logger.LauncherAtom.FolderIcon;
 import com.android.launcher3.logger.LauncherAtom.FromState;
 import com.android.launcher3.logger.LauncherAtom.ToState;
+import com.android.launcher3.logger.LauncherAtomExtensions.DeviceSearchResultContainer;
+import com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.AllAppsList;
@@ -46,6 +56,7 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.LogConfig;
+import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.SysUiStatsLog;
 
 import java.util.Optional;
@@ -72,6 +83,7 @@
     private static final int DEFAULT_PAGE_INDEX = -2;
     private static final int FOLDER_HIERARCHY_OFFSET = 100;
     private static final int SEARCH_RESULT_HIERARCHY_OFFSET = 200;
+    private static final int EXTENDED_CONTAINERS_HIERARCHY_OFFSET = 300;
 
     public static final CopyOnWriteArrayList<StatsLogConsumer> LOGS_CONSUMER =
             new CopyOnWriteArrayList<>();
@@ -83,8 +95,8 @@
     }
 
     @Override
-    public StatsLogger logger() {
-        return new StatsCompatLogger();
+    protected StatsLogger createLogger() {
+        return new StatsCompatLogger(mContext);
     }
 
     /**
@@ -126,6 +138,7 @@
 
         private static final ItemInfo DEFAULT_ITEM_INFO = new ItemInfo();
 
+        private Context mContext;
         private ItemInfo mItemInfo = DEFAULT_ITEM_INFO;
         private InstanceId mInstanceId = DEFAULT_INSTANCE_ID;
         private OptionalInt mRank = OptionalInt.empty();
@@ -135,6 +148,11 @@
         private Optional<FromState> mFromState = Optional.empty();
         private Optional<ToState> mToState = Optional.empty();
         private Optional<String> mEditText = Optional.empty();
+        private SliceItem mSliceItem;
+
+        StatsCompatLogger(Context context) {
+            mContext = context;
+        }
 
         @Override
         public StatsLogger withItemInfo(ItemInfo itemInfo) {
@@ -172,10 +190,8 @@
 
         @Override
         public StatsLogger withContainerInfo(ContainerInfo containerInfo) {
-            if (mItemInfo != DEFAULT_ITEM_INFO) {
-                throw new IllegalArgumentException(
+            checkState(mItemInfo == DEFAULT_ITEM_INFO,
                         "ItemInfo and ContainerInfo are mutual exclusive; cannot log both.");
-            }
             this.mContainerInfo = Optional.of(containerInfo);
             return this;
         }
@@ -199,12 +215,33 @@
         }
 
         @Override
+        public StatsLogger withSliceItem(@NonNull SliceItem sliceItem) {
+            this.mSliceItem = checkNotNull(sliceItem, "expected valid sliceItem but received null");
+            checkState(mItemInfo == DEFAULT_ITEM_INFO,
+                    "ItemInfo and SliceItem are mutual exclusive; cannot log both.");
+            return this;
+        }
+
+        @Override
         public void log(EventEnum event) {
             if (!Utilities.ATLEAST_R) {
                 return;
             }
-
             LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
+
+            if (mSliceItem != null) {
+                Executors.MODEL_EXECUTOR.execute(
+                        () -> {
+                            LauncherAtom.ItemInfo.Builder itemInfoBuilder =
+                                    LauncherAtom.ItemInfo.newBuilder().setSlice(
+                                            LauncherAtom.Slice.newBuilder().setUri(
+                                                    mSliceItem.getSlice().getUri().toString()));
+                            mContainerInfo.ifPresent(itemInfoBuilder::setContainerInfo);
+                            write(event, applyOverwrites(itemInfoBuilder.build()));
+                        });
+                return;
+            }
+
             if (mItemInfo.container < 0 || appState == null) {
                 // Write log on the model thread so that logs do not go out of order
                 // (for eg: drop comes after drag)
@@ -225,6 +262,26 @@
             }
         }
 
+        @Override
+        public void sendToInteractionJankMonitor(EventEnum event, View view) {
+            if (!(event instanceof LauncherEvent)) {
+                return;
+            }
+            switch ((LauncherEvent) event) {
+                case LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN:
+                    InteractionJankMonitorWrapper.begin(
+                            view,
+                            InteractionJankMonitorWrapper.CUJ_ALL_APPS_SCROLL);
+                    break;
+                case LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END:
+                    InteractionJankMonitorWrapper.end(
+                            InteractionJankMonitorWrapper.CUJ_ALL_APPS_SCROLL);
+                    break;
+                default:
+                    break;
+            }
+        }
+
         private LauncherAtom.ItemInfo applyOverwrites(LauncherAtom.ItemInfo atomInfo) {
             LauncherAtom.ItemInfo.Builder itemInfoBuilder = atomInfo.toBuilder();
 
@@ -299,6 +356,14 @@
                 return info.getContainerInfo().getPredictedHotseatContainer().getCardinality();
             case SEARCH_RESULT_CONTAINER:
                 return info.getContainerInfo().getSearchResultContainer().getQueryLength();
+            case EXTENDED_CONTAINERS:
+                ExtendedContainers extendedCont = info.getContainerInfo().getExtendedContainers();
+                if (extendedCont.getContainerCase() == DEVICE_SEARCH_RESULT_CONTAINER) {
+                    DeviceSearchResultContainer deviceSearchResultCont = extendedCont
+                            .getDeviceSearchResultContainer();
+                    return deviceSearchResultCont.hasQueryLength() ? deviceSearchResultCont
+                            .getQueryLength() : -1;
+                }
             default:
                 return info.getFolderIcon().getCardinality();
         }
@@ -314,6 +379,8 @@
                 return info.getWidget().getPackageName();
             case TASK:
                 return info.getTask().getPackageName();
+            case SEARCH_ACTION_ITEM:
+                return info.getSearchActionItem().getPackageName();
             default:
                 return null;
         }
@@ -329,6 +396,10 @@
                 return info.getWidget().getComponentName();
             case TASK:
                 return info.getTask().getComponentName();
+            case SEARCH_ACTION_ITEM:
+                return info.getSearchActionItem().getTitle();
+            case SLICE:
+                return info.getSlice().getUri();
             default:
                 return null;
         }
@@ -397,6 +468,9 @@
         } else if (info.getContainerInfo().getContainerCase() == SEARCH_RESULT_CONTAINER) {
             return info.getContainerInfo().getSearchResultContainer().getParentContainerCase()
                     .getNumber() + SEARCH_RESULT_HIERARCHY_OFFSET;
+        } else if (info.getContainerInfo().getContainerCase() == EXTENDED_CONTAINERS) {
+            return info.getContainerInfo().getExtendedContainers().getContainerCase().getNumber()
+                    + EXTENDED_CONTAINERS_HIERARCHY_OFFSET;
         } else {
             return info.getContainerInfo().getContainerCase().getNumber();
         }
diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
index deb70e0..7f94839 100644
--- a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
+++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
@@ -17,7 +17,6 @@
 
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 
@@ -38,7 +37,6 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.quickstep.LauncherActivityInterface;
-import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.views.RecentsView;
 
 /**
@@ -49,12 +47,6 @@
  */
 public class AnimatorControllerWithResistance {
 
-    /**
-     * How much farther we can drag past overview in 2-button mode, as a factor of the distance
-     * it takes to drag from an app to overview.
-     */
-    public static final float TWO_BUTTON_EXTRA_DRAG_FACTOR = 0.25f;
-
     private enum RecentsResistanceParams {
         FROM_APP(0.75f, 0.5f, 1f),
         FROM_OVERVIEW(1f, 0.75f, 0.5f);
@@ -161,12 +153,6 @@
         LauncherActivityInterface.INSTANCE.calculateTaskSize(params.context, params.dp, startRect,
                 orientationHandler);
         long distanceToCover = startRect.bottom;
-        boolean isTwoButtonMode = SysUINavigationMode.getMode(params.context) == TWO_BUTTONS;
-        if (isTwoButtonMode) {
-            // We can only drag a small distance past overview, not to the top of the screen.
-            distanceToCover = (long)
-                    ((params.dp.heightPx - startRect.bottom) * TWO_BUTTON_EXTRA_DRAG_FACTOR);
-        }
         PendingAnimation resistAnim = params.resistAnim != null
                 ? params.resistAnim
                 : new PendingAnimation(distanceToCover * 2);
@@ -178,43 +164,35 @@
                 / (params.dp.heightPx - startRect.bottom);
         // This is what the scale would be at the end of the drag if we didn't apply resistance.
         float endScale = params.startScale - prevScaleRate * distanceToCover;
-        final TimeInterpolator scaleInterpolator;
-        if (isTwoButtonMode) {
-            // We are bounded by the distance of the drag, so we don't need to apply resistance.
-            scaleInterpolator = LINEAR;
-        } else {
-            // Create an interpolator that resists the scale so the scale doesn't get smaller than
-            // RECENTS_SCALE_MAX_RESIST.
-            float startResist = Utilities.getProgress(params.resistanceParams.scaleStartResist,
-                    params.startScale, endScale);
-            float maxResist = Utilities.getProgress(params.resistanceParams.scaleMaxResist,
-                    params.startScale, endScale);
-            scaleInterpolator = t -> {
-                if (t < startResist) {
-                    return t;
-                }
-                float resistProgress = Utilities.getProgress(t, startResist, 1);
-                resistProgress = RECENTS_SCALE_RESIST_INTERPOLATOR.getInterpolation(resistProgress);
-                return startResist + resistProgress * (maxResist - startResist);
-            };
-        }
+        // Create an interpolator that resists the scale so the scale doesn't get smaller than
+        // RECENTS_SCALE_MAX_RESIST.
+        float startResist = Utilities.getProgress(params.resistanceParams.scaleStartResist,
+                params.startScale, endScale);
+        float maxResist = Utilities.getProgress(params.resistanceParams.scaleMaxResist,
+                params.startScale, endScale);
+        final TimeInterpolator scaleInterpolator = t -> {
+            if (t < startResist) {
+                return t;
+            }
+            float resistProgress = Utilities.getProgress(t, startResist, 1);
+            resistProgress = RECENTS_SCALE_RESIST_INTERPOLATOR.getInterpolation(resistProgress);
+            return startResist + resistProgress * (maxResist - startResist);
+        };
         resistAnim.addFloat(params.scaleTarget, params.scaleProperty, params.startScale, endScale,
                 scaleInterpolator);
 
-        if (!isTwoButtonMode) {
-            // Compute where the task view would be based on the end scale, if we didn't translate.
-            RectF endRectF = new RectF(startRect);
-            Matrix temp = new Matrix();
-            temp.setScale(params.resistanceParams.scaleMaxResist,
-                    params.resistanceParams.scaleMaxResist, pivot.x, pivot.y);
-            temp.mapRect(endRectF);
-            // Translate such that the task view touches the top of the screen when drag does.
-            float endTranslation = endRectF.top
-                    * orientationHandler.getSecondaryTranslationDirectionFactor()
-                    * params.resistanceParams.translationFactor;
-            resistAnim.addFloat(params.translationTarget, params.translationProperty,
-                    params.startTranslation, endTranslation, RECENTS_TRANSLATE_RESIST_INTERPOLATOR);
-        }
+        // Compute where the task view would be based on the end scale.
+        RectF endRectF = new RectF(startRect);
+        Matrix temp = new Matrix();
+        temp.setScale(params.resistanceParams.scaleMaxResist,
+                params.resistanceParams.scaleMaxResist, pivot.x, pivot.y);
+        temp.mapRect(endRectF);
+        // Translate such that the task view touches the top of the screen when drag does.
+        float endTranslation = endRectF.top
+                * orientationHandler.getSecondaryTranslationDirectionFactor()
+                * params.resistanceParams.translationFactor;
+        resistAnim.addFloat(params.translationTarget, params.translationProperty,
+                params.startTranslation, endTranslation, RECENTS_TRANSLATE_RESIST_INTERPOLATOR);
 
         return resistAnim;
     }
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index 8cde5f2..8151d41 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -15,8 +15,6 @@
  */
 package com.android.quickstep.util;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_SYSTEM_VELOCITY_PROVIDER;
-
 import android.content.Context;
 import android.content.res.Resources;
 import android.view.MotionEvent;
@@ -51,7 +49,7 @@
     private final Alarm mForcePauseTimeout;
     private final boolean mMakePauseHarderToTrigger;
     private final Context mContext;
-    private final VelocityProvider mVelocityProvider;
+    private final SystemVelocityProvider mVelocityProvider;
 
     private Float mPreviousVelocity = null;
 
@@ -88,8 +86,7 @@
         mForcePauseTimeout = new Alarm();
         mForcePauseTimeout.setOnAlarmListener(alarm -> updatePaused(true /* isPaused */));
         mMakePauseHarderToTrigger = makePauseHarderToTrigger;
-        mVelocityProvider = ENABLE_SYSTEM_VELOCITY_PROVIDER.get()
-                ? new SystemVelocityProvider(axis) : new LinearVelocityProvider(axis);
+        mVelocityProvider = new SystemVelocityProvider(axis);
     }
 
     /**
@@ -124,8 +121,8 @@
         mForcePauseTimeout.setAlarm(mMakePauseHarderToTrigger
                 ? HARDER_TRIGGER_TIMEOUT
                 : FORCE_PAUSE_TIMEOUT);
-        Float newVelocity = mVelocityProvider.addMotionEvent(ev, pointerIndex);
-        if (newVelocity != null && mPreviousVelocity != null) {
+        float newVelocity = mVelocityProvider.addMotionEvent(ev, pointerIndex);
+        if (mPreviousVelocity != null) {
             checkMotionPaused(newVelocity, mPreviousVelocity, ev.getEventTime());
         }
         mPreviousVelocity = newVelocity;
@@ -210,58 +207,7 @@
         default void onMotionPauseChanged(boolean isPaused) { }
     }
 
-    /**
-     * Interface to abstract out velocity calculations
-     */
-    protected interface VelocityProvider {
-
-        /**
-         * Adds a new motion events, and returns the velocity at this point, or null if
-         * the velocity is not available
-         */
-        Float addMotionEvent(MotionEvent ev, int pointer);
-
-        /**
-         * Clears all stored motion event records
-         */
-        void clear();
-    }
-
-    private static class LinearVelocityProvider implements VelocityProvider {
-
-        private Long mPreviousTime = null;
-        private Float mPreviousPosition = null;
-
-        private final int mAxis;
-
-        LinearVelocityProvider(int axis) {
-            mAxis = axis;
-        }
-
-        @Override
-        public Float addMotionEvent(MotionEvent ev, int pointer) {
-            long time = ev.getEventTime();
-            float position = ev.getAxisValue(mAxis, pointer);
-            Float velocity = null;
-
-            if (mPreviousTime != null && mPreviousPosition != null) {
-                long changeInTime = Math.max(1, time - mPreviousTime);
-                float changeInPosition = position - mPreviousPosition;
-                velocity = changeInPosition / changeInTime;
-            }
-            mPreviousTime = time;
-            mPreviousPosition = position;
-            return velocity;
-        }
-
-        @Override
-        public void clear() {
-            mPreviousTime = null;
-            mPreviousPosition = null;
-        }
-    }
-
-    private static class SystemVelocityProvider implements VelocityProvider {
+    private static class SystemVelocityProvider {
 
         private final VelocityTracker mVelocityTracker;
         private final int mAxis;
@@ -271,8 +217,11 @@
             mAxis = axis;
         }
 
-        @Override
-        public Float addMotionEvent(MotionEvent ev, int pointer) {
+        /**
+         * Adds a new motion events, and returns the velocity at this point, or null if
+         * the velocity is not available
+         */
+        public float addMotionEvent(MotionEvent ev, int pointer) {
             mVelocityTracker.addMovement(ev);
             mVelocityTracker.computeCurrentVelocity(1); // px / ms
             return mAxis == MotionEvent.AXIS_X
@@ -280,7 +229,9 @@
                     : mVelocityTracker.getYVelocity(pointer);
         }
 
-        @Override
+        /**
+         * Clears all stored motion event records
+         */
         public void clear() {
             mVelocityTracker.clear();
         }
diff --git a/quickstep/src/com/android/quickstep/util/NavigationModeFeatureFlag.java b/quickstep/src/com/android/quickstep/util/NavigationModeFeatureFlag.java
new file mode 100644
index 0000000..60c7add
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/NavigationModeFeatureFlag.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util;
+
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+
+import android.content.Context;
+
+import com.android.quickstep.SysUINavigationMode;
+
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/** A feature flag that listens to navigation mode changes. */
+public class NavigationModeFeatureFlag implements
+        SysUINavigationMode.NavigationModeChangeListener {
+
+    public static final NavigationModeFeatureFlag LIVE_TILE = new NavigationModeFeatureFlag(
+            ENABLE_QUICKSTEP_LIVE_TILE::get, mode -> mode.hasGestures);
+
+    private final Supplier<Boolean> mBasePredicate;
+    private final Predicate<SysUINavigationMode.Mode> mModePredicate;
+    private boolean mSupported;
+
+    private NavigationModeFeatureFlag(Supplier<Boolean> basePredicate,
+            Predicate<SysUINavigationMode.Mode> modePredicate) {
+        mBasePredicate = basePredicate;
+        mModePredicate = modePredicate;
+    }
+
+    public boolean get() {
+        return mBasePredicate.get() && mSupported;
+    }
+
+    public void initialize(Context context) {
+        onNavigationModeChanged(SysUINavigationMode.INSTANCE.get(context).getMode());
+        SysUINavigationMode.INSTANCE.get(context).addModeChangeListener(this);
+    }
+
+    @Override
+    public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
+        mSupported = mModePredicate.test(newMode);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
index cb81d36..1544f00 100644
--- a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
+++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
@@ -27,10 +27,8 @@
 
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.allapps.AllAppsInsetTransitionController;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.hybridhotseat.HotseatPredictionController;
-import com.android.launcher3.search.DeviceSearchEdu;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -43,7 +41,6 @@
  */
 public class QuickstepOnboardingPrefs extends OnboardingPrefs<QuickstepLauncher> {
 
-
     public QuickstepOnboardingPrefs(QuickstepLauncher launcher, SharedPreferences sharedPrefs) {
         super(launcher, sharedPrefs);
 
@@ -66,8 +63,7 @@
             });
         }
 
-        if (FeatureFlags.ENABLE_HYBRID_HOTSEAT.get() && !hasReachedMaxCount(
-                HOTSEAT_DISCOVERY_TIP_COUNT)) {
+        if (!hasReachedMaxCount(HOTSEAT_DISCOVERY_TIP_COUNT)) {
             stateManager.addStateListener(new StateListener<LauncherState>() {
                 boolean mFromAllApps = false;
 
@@ -134,22 +130,5 @@
                 }
             });
         }
-
-        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && !getBoolean(SEARCH_EDU_SEEN)) {
-            stateManager.addStateListener(new StateListener<LauncherState>() {
-                @Override
-                public void onStateTransitionStart(LauncherState toState) {
-                    if (toState == ALL_APPS) {
-                        AllAppsInsetTransitionController insetTransitionController =
-                                mLauncher.getAllAppsController().getInsetController();
-                        insetTransitionController.setSearchEduRunnable(() -> {
-                            DeviceSearchEdu.show(launcher);
-                            insetTransitionController.setSearchEduRunnable(null);
-                        });
-                        stateManager.removeStateListener(this);
-                    }
-                }
-            });
-        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index a3ee912..215f05a 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -23,22 +23,18 @@
 import static android.view.Surface.ROTATION_270;
 import static android.view.Surface.ROTATION_90;
 
-import static com.android.launcher3.Utilities.newContentObserver;
+import static com.android.launcher3.util.SettingsCache.ROTATION_SETTING_URI;
 import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.res.Resources;
-import android.database.ContentObserver;
 import android.graphics.Matrix;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.os.Handler;
-import android.provider.Settings;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.OrientationEventListener;
@@ -51,6 +47,7 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.WindowBounds;
@@ -72,9 +69,6 @@
     private static final String TAG = "RecentsOrientedState";
     private static final boolean DEBUG = false;
 
-    private ContentObserver mSystemAutoRotateObserver =
-            newContentObserver(new Handler(), t -> updateAutoRotateSetting());
-
     @Retention(SOURCE)
     @IntDef({ROTATION_0, ROTATION_90, ROTATION_180, ROTATION_270})
     public @interface SurfaceRotation {}
@@ -118,9 +112,11 @@
                     | FLAG_SWIPE_UP_NOT_RUNNING;
 
     private final Context mContext;
-    private final ContentResolver mContentResolver;
     private final SharedPreferences mSharedPrefs;
     private final OrientationEventListener mOrientationListener;
+    private final SettingsCache mSettingsCache;
+    private final SettingsCache.OnChangeListener mRotationChangeListener =
+            isEnabled -> updateAutoRotateSetting();
 
     private final Matrix mTmpMatrix = new Matrix();
 
@@ -138,7 +134,6 @@
     public RecentsOrientedState(Context context, BaseActivityInterface sizeStrategy,
             IntConsumer rotationChangeListener) {
         mContext = context;
-        mContentResolver = context.getContentResolver();
         mSharedPrefs = Utilities.getPrefs(context);
         mOrientationListener = new OrientationEventListener(context) {
             @Override
@@ -162,6 +157,7 @@
             mFlags |= FLAG_MULTIPLE_ORIENTATION_SUPPORTED_BY_DENSITY;
         }
         mFlags |= FLAG_SWIPE_UP_NOT_RUNNING;
+        mSettingsCache = SettingsCache.INSTANCE.get(mContext);
         initFlags();
     }
 
@@ -271,8 +267,8 @@
     }
 
     private void updateAutoRotateSetting() {
-        setFlag(FLAG_SYSTEM_ROTATION_ALLOWED, Settings.System.getInt(mContentResolver,
-                Settings.System.ACCELEROMETER_ROTATION, 1) == 1);
+        setFlag(FLAG_SYSTEM_ROTATION_ALLOWED,
+                mSettingsCache.getValue(ROTATION_SETTING_URI, 1));
     }
 
     private void updateHomeRotationSetting() {
@@ -295,9 +291,7 @@
     public void initListeners() {
         if (isMultipleOrientationSupportedByDevice()) {
             mSharedPrefs.registerOnSharedPreferenceChangeListener(this);
-            mContentResolver.registerContentObserver(
-                    Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION),
-                    false, mSystemAutoRotateObserver);
+            mSettingsCache.register(ROTATION_SETTING_URI, mRotationChangeListener);
         }
         initFlags();
     }
@@ -308,7 +302,7 @@
     public void destroyListeners() {
         if (isMultipleOrientationSupportedByDevice()) {
             mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
-            mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver);
+            mSettingsCache.unregister(ROTATION_SETTING_URI, mRotationChangeListener);
         }
         setRotationWatcherEnabled(false);
     }
diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
index 19c6588..3adb459 100644
--- a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
+++ b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java
@@ -43,8 +43,11 @@
             }
 
             @Override
-            public void onCreateAnimation(RemoteAnimationTargetCompat[] appTargets,
-                    RemoteAnimationTargetCompat[] wallpaperTargets, AnimationResult result) {
+            public void onCreateAnimation(int transit,
+                    RemoteAnimationTargetCompat[] appTargets,
+                    RemoteAnimationTargetCompat[] wallpaperTargets,
+                    RemoteAnimationTargetCompat[] nonApps,
+                    AnimationResult result) {
                 result.setAnimation(createWindowAnimation(appTargets, wallpaperTargets), context);
             }
         };
diff --git a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
index 4120331..932ff27 100644
--- a/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
+++ b/quickstep/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_COMPONENTS;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_DEPTH_CONTROLLER;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
+import static com.android.launcher3.states.StateAnimationConfig.SKIP_TASKBAR;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -138,7 +139,7 @@
 
         addDepthAnimationForState(launcher, NORMAL, ALPHA_DURATION_MS);
 
-        mAnimators.play(launcher.getDragLayer().getScrim().createSysuiMultiplierAnim(0f, 1f)
+        mAnimators.play(launcher.getDragLayer().getSysUiScrim().createSysuiMultiplierAnim(0f, 1f)
                 .setDuration(ALPHA_DURATION_MS));
         mAnimators.addListener(new AnimatorListenerAdapter() {
             @Override
@@ -158,7 +159,8 @@
      */
     private void prepareToAnimate(Launcher launcher, boolean animateOverviewScrim) {
         StateAnimationConfig config = new StateAnimationConfig();
-        config.animFlags = ANIM_ALL_COMPONENTS | SKIP_OVERVIEW | SKIP_DEPTH_CONTROLLER;
+        config.animFlags = ANIM_ALL_COMPONENTS | SKIP_OVERVIEW | SKIP_DEPTH_CONTROLLER
+                | SKIP_TASKBAR;
         config.duration = 0;
         // setRecentsAttachedToAppWindow() will animate recents out.
         launcher.getStateManager().createAtomicAnimation(BACKGROUND_APP, NORMAL, config).start();
diff --git a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
index 378f25b..0ce5072 100644
--- a/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
@@ -31,6 +31,7 @@
 import android.view.View;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.systemui.shared.pip.PipSurfaceTransactionHelper;
@@ -60,7 +61,7 @@
     /** for calculating the transform in {@link #onAnimationUpdate(ValueAnimator)} */
     private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect());
     private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect());
-    private final Rect mSourceHintRectInsets = new Rect();
+    private final Rect mSourceHintRectInsets;
     private final Rect mSourceInsets = new Rect();
 
     /** for rotation via {@link #setFromRotation(TaskViewSimulator, int)} */
@@ -89,7 +90,7 @@
     public SwipePipToHomeAnimator(int taskId,
             @NonNull ComponentName componentName,
             @NonNull SurfaceControl leash,
-            @NonNull Rect sourceRectHint,
+            @Nullable Rect sourceRectHint,
             @NonNull Rect appBounds,
             @NonNull Rect startBounds,
             @NonNull Rect destinationBounds,
@@ -104,10 +105,14 @@
         mDestinationBoundsAnimation.set(mDestinationBounds);
         mSurfaceTransactionHelper = new PipSurfaceTransactionHelper();
 
-        mSourceHintRectInsets.set(sourceRectHint.left - appBounds.left,
-                sourceRectHint.top - appBounds.top,
-                appBounds.right - sourceRectHint.right,
-                appBounds.bottom - sourceRectHint.bottom);
+        if (sourceRectHint == null) {
+            mSourceHintRectInsets = null;
+        } else {
+            mSourceHintRectInsets = new Rect(sourceRectHint.left - appBounds.left,
+                    sourceRectHint.top - appBounds.top,
+                    appBounds.right - sourceRectHint.right,
+                    appBounds.bottom - sourceRectHint.bottom);
+        }
 
         addListener(new AnimationSuccessListener() {
             @Override
@@ -168,34 +173,44 @@
         final float fraction = animator.getAnimatedFraction();
         final Rect bounds = mRectEvaluator.evaluate(fraction, mStartBounds,
                 mDestinationBoundsAnimation);
-        final Rect insets = mInsetsEvaluator.evaluate(fraction, mSourceInsets,
-                mSourceHintRectInsets);
         final SurfaceControl.Transaction tx =
                 PipSurfaceTransactionHelper.newSurfaceControlTransaction();
-        if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
-            final float degree, positionX, positionY;
-            if (mFromRotation == Surface.ROTATION_90) {
-                degree = -90 * fraction;
-                positionX = fraction * (mDestinationBoundsTransformed.left - mAppBounds.left)
-                        + mAppBounds.left;
-                positionY = fraction * (mDestinationBoundsTransformed.bottom - mAppBounds.top)
-                        + mAppBounds.top;
-            } else {
-                degree = 90 * fraction;
-                positionX = fraction * (mDestinationBoundsTransformed.right - mAppBounds.left)
-                        + mAppBounds.left;
-                positionY = fraction * (mDestinationBoundsTransformed.top - mAppBounds.top)
-                        + mAppBounds.top;
-            }
-            mSurfaceTransactionHelper.scaleAndRotate(tx, mLeash, mAppBounds, bounds, insets,
-                    degree, positionX, positionY);
+        if (mSourceHintRectInsets == null) {
+            // no source rect hint been set, directly scale the window down
+            onAnimationScale(fraction, tx, bounds);
         } else {
-            mSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mAppBounds, bounds, insets);
+            // scale and crop according to the source rect hint
+            onAnimationScaleAndCrop(fraction, tx, bounds);
         }
         mSurfaceTransactionHelper.resetCornerRadius(tx, mLeash);
         tx.apply();
     }
 
+    /** scale the window directly with no source rect hint being set */
+    private void onAnimationScale(float fraction, SurfaceControl.Transaction tx, Rect bounds) {
+        if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
+            final RotatedPosition rotatedPosition = getRotatedPosition(fraction);
+            mSurfaceTransactionHelper.scale(tx, mLeash, mAppBounds, bounds,
+                    rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY);
+        } else {
+            mSurfaceTransactionHelper.scale(tx, mLeash, mAppBounds, bounds);
+        }
+    }
+
+    /** scale and crop the window with source rect hint */
+    private void onAnimationScaleAndCrop(float fraction, SurfaceControl.Transaction tx,
+            Rect bounds) {
+        final Rect insets = mInsetsEvaluator.evaluate(fraction, mSourceInsets,
+                mSourceHintRectInsets);
+        if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
+            final RotatedPosition rotatedPosition = getRotatedPosition(fraction);
+            mSurfaceTransactionHelper.scaleAndRotate(tx, mLeash, mAppBounds, bounds, insets,
+                    rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY);
+        } else {
+            mSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mAppBounds, bounds, insets);
+        }
+    }
+
     public int getTaskId() {
         return mTaskId;
     }
@@ -217,4 +232,34 @@
         tx.apply();
         mHasAnimationEnded = true;
     }
+
+    private RotatedPosition getRotatedPosition(float fraction) {
+        final float degree, positionX, positionY;
+        if (mFromRotation == Surface.ROTATION_90) {
+            degree = -90 * fraction;
+            positionX = fraction * (mDestinationBoundsTransformed.left - mAppBounds.left)
+                    + mAppBounds.left;
+            positionY = fraction * (mDestinationBoundsTransformed.bottom - mAppBounds.top)
+                    + mAppBounds.top;
+        } else {
+            degree = 90 * fraction;
+            positionX = fraction * (mDestinationBoundsTransformed.right - mAppBounds.left)
+                    + mAppBounds.left;
+            positionY = fraction * (mDestinationBoundsTransformed.top - mAppBounds.top)
+                    + mAppBounds.top;
+        }
+        return new RotatedPosition(degree, positionX, positionY);
+    }
+
+    private static class RotatedPosition {
+        private final float degree;
+        private final float positionX;
+        private final float positionY;
+
+        private RotatedPosition(float degree, float positionX, float positionY) {
+            this.degree = degree;
+            this.positionX = positionX;
+            this.positionY = positionY;
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 65bcf26..9537247 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -15,15 +15,17 @@
  */
 package com.android.quickstep.util;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.states.RotationHelper.deltaRotation;
 import static com.android.launcher3.touch.PagedOrientationHandler.MATRIX_POST_TRANSLATE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
 import static com.android.quickstep.util.RecentsOrientedState.preDisplayRotation;
 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
 
 import android.animation.TimeInterpolator;
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Matrix;
 import android.graphics.Point;
 import android.graphics.PointF;
@@ -34,6 +36,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.PagedOrientationHandler;
@@ -41,7 +44,6 @@
 import com.android.quickstep.BaseActivityInterface;
 import com.android.quickstep.views.RecentsView.ScrollState;
 import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper;
-import com.android.quickstep.views.TaskView;
 import com.android.quickstep.views.TaskView.FullscreenDrawParams;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -74,6 +76,7 @@
 
     @NonNull
     private RecentsOrientedState mOrientationState;
+    private final boolean mIsRecentsRtl;
 
     private final Rect mTaskRect = new Rect();
     private boolean mDrawsBelowRecents;
@@ -92,20 +95,23 @@
 
     // TaskView properties
     private final FullscreenDrawParams mCurrentFullscreenParams;
-    private float mCurveScale = 1;
     public final AnimatedFloat taskPrimaryTranslation = new AnimatedFloat();
     public final AnimatedFloat taskSecondaryTranslation = new AnimatedFloat();
+    public final AnimatedFloat gridTranslationSecondary = new AnimatedFloat();
 
     // RecentsView properties
     public final AnimatedFloat recentsViewScale = new AnimatedFloat();
     public final AnimatedFloat fullScreenProgress = new AnimatedFloat();
     public final AnimatedFloat recentsViewSecondaryTranslation = new AnimatedFloat();
+    public final AnimatedFloat gridProgress = new AnimatedFloat();
     private final ScrollState mScrollState = new ScrollState();
 
     // Cached calculations
     private boolean mLayoutValid = false;
     private boolean mScrollValid = false;
     private int mOrientationStateId;
+    private final int mTaskThumbnailPadding;
+    private final int mRowSpacing;
 
     public TaskViewSimulator(Context context, BaseActivityInterface sizeStrategy) {
         mContext = context;
@@ -115,6 +121,10 @@
         mOrientationState.setGestureActive(true);
         mCurrentFullscreenParams = new FullscreenDrawParams(context);
         mOrientationStateId = mOrientationState.getStateId();
+        Resources resources = context.getResources();
+        mIsRecentsRtl = mOrientationState.getOrientationHandler().getRecentsRtlSetting(resources);
+        mTaskThumbnailPadding = (int) resources.getDimension(R.dimen.task_thumbnail_top_margin);
+        mRowSpacing = (int) resources.getDimension(R.dimen.recents_row_spacing);
     }
 
     /**
@@ -276,13 +286,13 @@
             int start = mOrientationState.getOrientationHandler()
                     .getPrimaryValue(mTaskRect.left, mTaskRect.top);
             mScrollState.screenCenter = start + mScrollState.scroll + mScrollState.halfPageSize;
-            mScrollState.updateInterpolation(start);
-            mCurveScale = TaskView.getCurveScaleForInterpolation(mScrollState.linearInterpolation);
+            mScrollState.updateInterpolation(mDp, start);
         }
 
-        float progress = Utilities.boundToRange(fullScreenProgress.value, 0, 1);
+        float fullScreenProgress = Utilities.boundToRange(this.fullScreenProgress.value, 0, 1);
         mCurrentFullscreenParams.setProgress(
-                progress, recentsViewScale.value, mTaskRect.width(), mDp, mPositionHelper);
+                fullScreenProgress, recentsViewScale.value, mTaskRect.width(), mDp,
+                mPositionHelper);
 
         // Apply thumbnail matrix
         RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets;
@@ -294,8 +304,25 @@
         mMatrix.postTranslate(insets.left, insets.top);
         mMatrix.postScale(scale, scale);
 
-        // Apply TaskView matrix: scale, translate, scroll
-        mMatrix.postScale(mCurveScale, mCurveScale, taskWidth / 2, taskHeight / 2);
+        float interpolatedGridProgress = ACCEL_DEACCEL.getInterpolation(gridProgress.value);
+
+        // Apply TaskView matrix: gridProgress
+        final int boxLength = (int) Math.max(taskWidth, taskHeight);
+        float availableHeight =
+                mTaskThumbnailPadding + taskHeight + mSizeStrategy.getOverviewActionsHeight(
+                        mContext);
+        float rowHeight = (availableHeight - mRowSpacing) / 2;
+        float gridScale = rowHeight / (boxLength + mTaskThumbnailPadding);
+        scale = Utilities.mapRange(interpolatedGridProgress, 1f, gridScale);
+        mMatrix.postScale(scale, scale, mIsRecentsRtl ? 0 : taskWidth, 0);
+        float taskWidthDiff = taskWidth * (1 - gridScale);
+        float taskWidthOffset = mIsRecentsRtl ? taskWidthDiff : -taskWidthDiff;
+        mOrientationState.getOrientationHandler().set(mMatrix, MATRIX_POST_TRANSLATE,
+                Utilities.mapRange(interpolatedGridProgress, 0, taskWidthOffset));
+        mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
+                Utilities.mapRange(interpolatedGridProgress, 0, gridTranslationSecondary.value));
+
+        // Apply TaskView matrix: translate, scroll
         mMatrix.postTranslate(mTaskRect.left, mTaskRect.top);
         mOrientationState.getOrientationHandler().set(mMatrix, MATRIX_POST_TRANSLATE,
                 taskPrimaryTranslation.value);
@@ -327,7 +354,7 @@
                 .withCornerRadius(getCurrentCornerRadius())
                 .withShadowRadius(app.isTranslucent ? 0 : params.getShadowRadius());
 
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && params.getRecentsSurface() != null) {
+        if (LIVE_TILE.get() && params.getRecentsSurface() != null) {
             // When relativeLayer = 0, it reverts the surfaces back to the original order.
             builder.withRelativeLayerTo(params.getRecentsSurface(),
                     mDrawsBelowRecents ? Integer.MIN_VALUE : 0);
diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
index 0837300..e7101cc 100644
--- a/quickstep/src/com/android/quickstep/views/ClearAllButton.java
+++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
@@ -43,9 +43,12 @@
     private float mScrollAlpha = 1;
     private float mContentAlpha = 1;
     private float mVisibilityAlpha = 1;
+    private float mGridProgress = 1;
 
     private boolean mIsRtl;
     private final float mOriginalTranslationX, mOriginalTranslationY;
+    private float mNormalTranslationPrimary;
+    private float mGridTranslationPrimary;
 
     private int mScrollOffset;
 
@@ -100,10 +103,18 @@
             return;
         }
 
-        float shift = Math.min(scrollState.scrollFromEdge, orientationSize);
-        float translation = mIsRtl ? (mScrollOffset - shift) : (mScrollOffset + shift);
-        orientationHandler.setPrimaryAndResetSecondaryTranslate(
-                this, translation, mOriginalTranslationX, mOriginalTranslationY);
+        float shift;
+        if (mIsRtl) {
+            shift = Math.min(scrollState.scrollFromEdge, orientationSize);
+        } else {
+            shift = Math.min(scrollState.scrollFromEdge,
+                    orientationSize + getGridTrans(mGridTranslationPrimary))
+                    - getGridTrans(mGridTranslationPrimary);
+        }
+        mNormalTranslationPrimary = mIsRtl ? (mScrollOffset - shift) : (mScrollOffset + shift);
+        applyPrimaryTranslation();
+        orientationHandler.getSecondaryViewTranslate().set(this,
+                orientationHandler.getSecondaryValue(mOriginalTranslationX, mOriginalTranslationY));
         mScrollAlpha = 1 - shift / orientationSize;
         updateAlpha();
     }
@@ -111,6 +122,48 @@
     private void updateAlpha() {
         final float alpha = mScrollAlpha * mContentAlpha * mVisibilityAlpha;
         setAlpha(alpha);
-        setClickable(alpha == 1);
+        setClickable(Math.min(alpha, 1) == 1);
+    }
+
+    public void setGridTranslationPrimary(float gridTranslationPrimary) {
+        mGridTranslationPrimary = gridTranslationPrimary;
+        applyPrimaryTranslation();
+    }
+
+    public float getScrollAdjustment(boolean gridEnabled) {
+        float scrollAdjustment = 0;
+        if (gridEnabled) {
+            scrollAdjustment += mGridTranslationPrimary;
+        }
+        return scrollAdjustment;
+    }
+
+    public float getOffsetAdjustment(boolean gridEnabled) {
+        return getScrollAdjustment(gridEnabled);
+    }
+
+    /**
+     * Moves ClearAllButton between carousel and 2 row grid.
+     *
+     * @param gridProgress 0 = carousel; 1 = 2 row grid.
+     */
+    public void setGridProgress(float gridProgress) {
+        mGridProgress = gridProgress;
+        applyPrimaryTranslation();
+    }
+
+    private void applyPrimaryTranslation() {
+        RecentsView recentsView = getRecentsView();
+        if (recentsView == null) {
+            return;
+        }
+
+        PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
+        orientationHandler.getPrimaryViewTranslate().set(this,
+                mNormalTranslationPrimary + getGridTrans(mGridTranslationPrimary));
+    }
+
+    private float getGridTrans(float endTranslation) {
+        return mGridProgress > 0 ? endTranslation : 0;
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
index 6630aed..25ae055 100644
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
@@ -71,6 +71,7 @@
     private ViewOutlineProvider mOldBannerOutlineProvider;
     private float mBannerOffsetPercentage;
     private float mBannerAlpha = 1f;
+    private float mVerticalOffset = 0f;
 
     public DigitalWellBeingToast(BaseDraggingActivity activity, TaskView taskView) {
         mActivity = activity;
@@ -275,16 +276,17 @@
             @Override
             public void getOutline(View view, Outline outline) {
                 mOldBannerOutlineProvider.getOutline(view, outline);
-                outline.offset(0, -Math.round(view.getTranslationY()));
+                outline.offset(0, Math.round(-view.getTranslationY() + mVerticalOffset));
             }
         });
         mBanner.setClipToOutline(true);
     }
 
-    void updateBannerOffset(float offsetPercentage) {
+    void updateBannerOffset(float offsetPercentage, float verticalOffset) {
         if (mBanner != null && mBannerOffsetPercentage != offsetPercentage) {
+            mVerticalOffset = verticalOffset;
             mBannerOffsetPercentage = offsetPercentage;
-            mBanner.setTranslationY(offsetPercentage * mBanner.getHeight());
+            mBanner.setTranslationY(offsetPercentage * mBanner.getHeight() + mVerticalOffset);
             mBanner.invalidateOutline();
         }
     }
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 52a7466..c62f3e2 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -16,14 +16,14 @@
 package com.android.quickstep.views;
 
 import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
+import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
 import static com.android.launcher3.QuickstepAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
@@ -95,7 +95,7 @@
     public void startHome() {
         Runnable onReachedHome = () -> mActivity.getStateManager().goToState(NORMAL, false);
         OverviewToHomeAnim overviewToHomeAnim = new OverviewToHomeAnim(mActivity, onReachedHome);
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             switchToScreenshot(null,
                     () -> finishRecentsAnimation(true /* toRecents */,
                             () -> overviewToHomeAnim.animateWithVelocity(0)));
@@ -160,6 +160,8 @@
             reset();
         }
         setOverlayEnabled(finalState == OVERVIEW || finalState == OVERVIEW_MODAL_TASK);
+        setOverviewGridEnabled(finalState.displayOverviewTasksAsGrid(mActivity));
+        setOverviewFullscreenEnabled(finalState.getOverviewFullscreenProgress() == 1);
         setFreezeViewVisibility(false);
     }
 
@@ -169,7 +171,7 @@
         if (enabled) {
             LauncherState state = mActivity.getStateManager().getState();
             boolean hasClearAllButton = (state.getVisibleElements(mActivity)
-                    & OVERVIEW_BUTTONS) != 0;
+                    & CLEAR_ALL_BUTTON) != 0;
             setDisallowScrollToClearAll(!hasClearAllButton);
         }
     }
diff --git a/quickstep/src/com/android/quickstep/views/LiveTileOverlay.java b/quickstep/src/com/android/quickstep/views/LiveTileOverlay.java
deleted file mode 100644
index 747c3f2..0000000
--- a/quickstep/src/com/android/quickstep/views/LiveTileOverlay.java
+++ /dev/null
@@ -1,185 +0,0 @@
-package com.android.quickstep.views;
-
-import static android.view.Surface.ROTATION_0;
-import static android.view.Surface.ROTATION_180;
-import static android.view.Surface.ROTATION_270;
-import static android.view.Surface.ROTATION_90;
-
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.Drawable;
-import android.util.FloatProperty;
-import android.view.ViewOverlay;
-
-import com.android.launcher3.anim.Interpolators;
-import com.android.quickstep.util.RecentsOrientedState.SurfaceRotation;
-
-public class LiveTileOverlay extends Drawable {
-
-    private static final long ICON_ANIM_DURATION = 120;
-
-    private static final FloatProperty<LiveTileOverlay> PROGRESS =
-            new FloatProperty<LiveTileOverlay>("progress") {
-                @Override
-                public void setValue(LiveTileOverlay liveTileOverlay, float progress) {
-                    liveTileOverlay.setIconAnimationProgress(progress);
-                }
-
-                @Override
-                public Float get(LiveTileOverlay liveTileOverlay) {
-                    return liveTileOverlay.mIconAnimationProgress;
-                }
-            };
-
-    public static final LiveTileOverlay INSTANCE = new LiveTileOverlay();
-
-    private final Paint mPaint = new Paint();
-    private final RectF mCurrentRect = new RectF();
-    private final Rect mBoundsRect = new Rect();
-
-    private @SurfaceRotation int mRotation = ROTATION_0;
-
-    private float mCornerRadius;
-    private Drawable mIcon;
-    private Animator mIconAnimator;
-
-    private float mIconAnimationProgress = 0f;
-    private boolean mIsAttached;
-
-    private LiveTileOverlay() {
-        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
-    }
-
-    public void update(RectF currentRect, float cornerRadius) {
-        invalidateSelf();
-
-        mCurrentRect.set(currentRect);
-        mCornerRadius = cornerRadius;
-
-        mCurrentRect.roundOut(mBoundsRect);
-        setBounds(mBoundsRect);
-        invalidateSelf();
-    }
-
-    public void update(float left, float top, float right, float bottom) {
-        mCurrentRect.set(left, top, right, bottom);
-    }
-
-    public void setRotation(@SurfaceRotation int rotation) {
-        mRotation = rotation;
-    }
-
-    public void setIcon(Drawable icon) {
-        mIcon = icon;
-    }
-
-    public void startIconAnimation() {
-        if (mIconAnimator != null) {
-            mIconAnimator.cancel();
-        }
-        // This animator must match the icon part of {@link TaskView#FOCUS_TRANSITION} animation.
-        mIconAnimator = ObjectAnimator.ofFloat(this, PROGRESS, 1);
-        mIconAnimator.setDuration(ICON_ANIM_DURATION).setInterpolator(LINEAR);
-        mIconAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mIconAnimator = null;
-            }
-        });
-        mIconAnimator.start();
-    }
-
-    public float cancelIconAnimation() {
-        if (mIconAnimator != null) {
-            mIconAnimator.cancel();
-        }
-        return mIconAnimationProgress;
-    }
-
-    @Override
-    public void draw(Canvas canvas) {
-        canvas.drawRoundRect(mCurrentRect, mCornerRadius, mCornerRadius, mPaint);
-        if (mIcon != null && mIconAnimationProgress > 0f) {
-            canvas.save();
-            float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, 0f,
-                    1f).getInterpolation(mIconAnimationProgress);
-
-            int iconRadius = mIcon.getBounds().width() / 2;
-            float dx = 0;
-            float dy = 0;
-
-            switch (mRotation) {
-                case ROTATION_0:
-                    dx = mCurrentRect.centerX() - iconRadius * scale;
-                    dy = mCurrentRect.top - iconRadius * scale;
-                    break;
-                case ROTATION_90:
-                    dx = mCurrentRect.right - iconRadius * scale;
-                    dy = mCurrentRect.centerY() - iconRadius * scale;
-                    break;
-                case ROTATION_270:
-                    dx = mCurrentRect.left - iconRadius * scale;
-                    dy = mCurrentRect.centerY() - iconRadius * scale;
-                    break;
-                case ROTATION_180:
-                    dx = mCurrentRect.centerX() - iconRadius * scale;
-                    dy = mCurrentRect.bottom - iconRadius * scale;
-                    break;
-            }
-
-            int rotationDegrees = mRotation * 90;
-            if (mRotation == ROTATION_90 || mRotation == ROTATION_270) {
-                canvas.rotate(rotationDegrees, dx + iconRadius, dy + iconRadius);
-            }
-            canvas.translate(dx, dy);
-            canvas.scale(scale, scale);
-            mIcon.draw(canvas);
-            canvas.restore();
-        }
-    }
-
-    @Override
-    public void setAlpha(int i) { }
-
-    @Override
-    public void setColorFilter(ColorFilter colorFilter) { }
-
-    @Override
-    public int getOpacity() {
-        return PixelFormat.TRANSLUCENT;
-    }
-
-    public boolean attach(ViewOverlay overlay) {
-        if (overlay != null && !mIsAttached) {
-            overlay.add(this);
-            mIsAttached = true;
-            return true;
-        }
-
-        return false;
-    }
-
-    public void detach(ViewOverlay overlay) {
-        if (overlay != null) {
-            overlay.remove(this);
-            mIsAttached = false;
-        }
-    }
-
-    private void setIconAnimationProgress(float progress) {
-        mIconAnimationProgress = progress;
-        invalidateSelf();
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index 8fb7e03..1241982 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -189,6 +189,14 @@
 
     /** Updates vertical margins for different navigation mode or configuration changes. */
     public void updateVerticalMargin(Mode mode) {
+        LayoutParams actionParams = (LayoutParams) findViewById(
+                R.id.action_buttons).getLayoutParams();
+        actionParams.setMargins(
+                actionParams.leftMargin, actionParams.topMargin, actionParams.rightMargin,
+                getBottomVerticalMargin(mode));
+    }
+
+    protected int getBottomVerticalMargin(Mode mode) {
         int bottomMargin;
         int orientation = getResources().getConfiguration().orientation;
         if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
@@ -201,8 +209,6 @@
                     .getDimensionPixelSize(R.dimen.overview_actions_bottom_margin_gesture);
         }
         bottomMargin += mInsets.bottom;
-        LayoutParams params = (LayoutParams) getLayoutParams();
-        params.setMargins(
-                params.leftMargin, params.topMargin, params.rightMargin, bottomMargin);
+        return bottomMargin;
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 2f2b566..bdd0a36 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -35,7 +35,6 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 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.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_CLEAR_ALL;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
@@ -43,6 +42,7 @@
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS;
@@ -72,6 +72,7 @@
 import android.text.TextPaint;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
+import android.util.Log;
 import android.util.SparseBooleanArray;
 import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
@@ -104,9 +105,11 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.touch.PagedOrientationHandler.CurveProperties;
 import com.android.launcher3.util.DynamicResource;
+import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.OverScroller;
 import com.android.launcher3.util.ResourceBasedOverride.Overrides;
@@ -239,6 +242,19 @@
                 }
             };
 
+    public static final FloatProperty<RecentsView> RECENTS_GRID_PROGRESS =
+            new FloatProperty<RecentsView>("recentsGrid") {
+                @Override
+                public void setValue(RecentsView view, float gridProgress) {
+                    view.setGridProgress(gridProgress);
+                }
+
+                @Override
+                public Float get(RecentsView view) {
+                    return view.mGridProgress;
+                }
+            };
+
     protected final RecentsOrientedState mOrientationState;
     protected final BaseActivityInterface mSizeStrategy;
     protected RecentsAnimationController mRecentsAnimationController;
@@ -264,6 +280,7 @@
     private final float mFastFlingVelocity;
     private final RecentsModel mModel;
     private final int mTaskTopMargin;
+    private final int mRowSpacing;
     private final ClearAllButton mClearAllButton;
     private final Rect mClearAllButtonDeadZoneRect = new Rect();
     private final Rect mTaskViewDeadZoneRect = new Rect();
@@ -282,9 +299,14 @@
     protected boolean mDisallowScrollToClearAll;
     private boolean mOverlayEnabled;
     protected boolean mFreezeViewVisibility;
+    private boolean mOverviewGridEnabled;
+    private boolean mOverviewFullscreenEnabled;
 
     private float mAdjacentPageOffset = 0;
     private float mTaskViewsSecondaryTranslation = 0;
+    // Progress from 0 to 1 where 0 is a carousel and 1 is a 2 row grid.
+    private float mGridProgress = 0;
+    private boolean mShowAsGrid;
 
     /**
      * TODO: Call reloadIdNeeded in onTaskStackChanged.
@@ -402,7 +424,6 @@
     private boolean mShowEmptyMessage;
     private OnEmptyMessageUpdatedListener mOnEmptyMessageUpdatedListener;
     private Layout mEmptyTextLayout;
-    private boolean mLiveTileOverlayAttached;
 
     // Keeps track of the index where the first TaskView should be
     private int mTaskViewStartIndex = 0;
@@ -450,6 +471,7 @@
         setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
         mTaskTopMargin = getResources()
                 .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
+        mRowSpacing = (int) getResources().getDimension(R.dimen.recents_row_spacing);
         mSquaredTouchSlop = squaredTouchSlop(context);
 
         mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents);
@@ -479,6 +501,11 @@
         mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
         mLiveTileTaskViewSimulator.setOrientationState(mOrientationState);
         mLiveTileTaskViewSimulator.setDrawsBelowRecents(true);
+
+        mShowAsGrid =
+                mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get();
+        mActivity.addOnDeviceProfileChangeListener(newDp ->
+                mShowAsGrid = newDp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get());
     }
 
     public OverScroller getScroller() {
@@ -537,7 +564,7 @@
     @Override
     protected void onWindowVisibilityChanged(int visibility) {
         super.onWindowVisibilityChanged(visibility);
-        if (visibility == GONE && ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (visibility != VISIBLE && LIVE_TILE.get()) {
             finishRecentsAnimation(true /* toRecents */, null);
         }
         updateTaskStackListenerState();
@@ -640,15 +667,31 @@
     }
 
     public boolean isTaskViewVisible(TaskView tv) {
-        // For now, just check if it's the active task or an adjacent task
-        return Math.abs(indexOfChild(tv) - getNextPage()) <= 1;
+        if (mShowAsGrid) {
+            int screenStart = mOrientationHandler.getPrimaryScroll(this);
+            int screenEnd = screenStart + mOrientationHandler.getMeasuredSize(this);
+            return isTaskViewWithinBounds(tv, screenStart, screenEnd);
+        } else {
+            // For now, just check if it's the active task or an adjacent task
+            return Math.abs(indexOfChild(tv) - getNextPage()) <= 1;
+        }
+    }
+
+    private boolean isTaskViewWithinBounds(TaskView tv, int start, int end) {
+        int taskStart = mOrientationHandler.getChildStart(tv) + (int) tv.getOffsetAdjustment(
+                mOverviewFullscreenEnabled, mOverviewGridEnabled);
+        int taskSize = (int) (mOrientationHandler.getMeasuredSize(tv) * tv.getSizeAdjustment(
+                mOverviewFullscreenEnabled, mOverviewGridEnabled));
+        int taskEnd = taskStart + taskSize;
+        return (taskStart >= start && taskStart <= end) || (taskEnd >= start
+                && taskEnd <= end);
     }
 
     public TaskView getTaskView(int taskId) {
         for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView tv = getTaskViewAt(i);
-            if (tv.getTask() != null && tv.getTask().key != null && tv.getTask().key.id == taskId) {
-                return tv;
+            TaskView taskView = getTaskViewAt(i);
+            if (taskView.hasTaskId(taskId)) {
+                return taskView;
             }
         }
         return null;
@@ -703,10 +746,21 @@
     public boolean onTouchEvent(MotionEvent ev) {
         super.onTouchEvent(ev);
 
-        TaskView taskView = getCurrentPageTaskView();
-        if (taskView != null && taskView.offerTouchToChildren(ev)) {
-            // Keep consuming events to pass to delegate
-            return true;
+        if (mShowAsGrid) {
+            int taskCount = getTaskViewCount();
+            for (int i = 0; i < taskCount; i++) {
+                TaskView taskView = getTaskViewAt(i);
+                if (isTaskViewVisible(taskView) && taskView.offerTouchToChildren(ev)) {
+                    // Keep consuming events to pass to delegate
+                    return true;
+                }
+            }
+        } else {
+            TaskView taskView = getCurrentPageTaskView();
+            if (taskView != null && taskView.offerTouchToChildren(ev)) {
+                // Keep consuming events to pass to delegate
+                return true;
+            }
         }
 
         final int x = (int) ev.getX();
@@ -758,6 +812,11 @@
     }
 
     @Override
+    protected boolean snapToPageInFreeScroll() {
+        return !mShowAsGrid;
+    }
+
+    @Override
     protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
         // Enables swiping to the left or right only if the task overlay is not modal.
         if (!isModal()) {
@@ -769,6 +828,12 @@
     }
 
     protected void applyLoadPlan(ArrayList<Task> tasks) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.GET_RECENTS_FAILED, "applyLoadPlan: taskCount=" + tasks.size());
+            for (Task t : tasks) {
+                Log.d(TestProtocol.GET_RECENTS_FAILED, "\t" + t);
+            }
+        }
         if (mPendingAnimation != null) {
             mPendingAnimation.addEndListener(success -> applyLoadPlan(tasks));
             return;
@@ -809,6 +874,7 @@
             final TaskView taskView = (TaskView) getChildAt(pageIndex);
             taskView.bind(task, mOrientationState);
         }
+        updateTaskSize();
 
         if (mNextPage == INVALID_PAGE) {
             // Set the current page to the running task, but not if settling on new task.
@@ -829,12 +895,21 @@
         resetTaskVisuals();
         onTaskStackUpdated();
         updateEnabledOverlays();
+
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.GET_RECENTS_FAILED, "applyLoadPlan: taskViewCount="
+                    + getTaskViewCount());
+        }
     }
 
     private boolean isModal() {
         return mTaskModalness > 0;
     }
 
+    public boolean isLoadingTasks() {
+        return mModel.isLoadingTasksInBackground();
+    }
+
     private void removeTasksViewsAndClearAllButton() {
         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
             removeView(getTaskViewAt(i));
@@ -845,6 +920,12 @@
     }
 
     public int getTaskViewCount() {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.GET_RECENTS_FAILED, "getTaskViewCount:"
+                    + " numChildren=" + getChildCount()
+                    + " start=" + mTaskViewStartIndex
+                    + " clearAll=" + indexOfChild(mClearAllButton));
+        }
         int taskViewCount = getChildCount() - mTaskViewStartIndex;
         if (indexOfChild(mClearAllButton) != -1) {
             taskViewCount--;
@@ -867,7 +948,7 @@
                 taskView.setModalness(mTaskModalness);
             }
         }
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             // Since we reuse the same mLiveTileTaskViewSimulator in the RecentsView, we need
             // to reset the params after it settles in Overview from swipe up so that we don't
             // render with obsolete param values.
@@ -875,10 +956,6 @@
             mLiveTileTaskViewSimulator.taskSecondaryTranslation.value = 0;
             mLiveTileTaskViewSimulator.fullScreenProgress.value = 0;
             mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
-
-            // Reset the live tile rect
-            DeviceProfile deviceProfile = mActivity.getDeviceProfile();
-            LiveTileOverlay.INSTANCE.update(0, 0, deviceProfile.widthPx, deviceProfile.heightPx);
         }
         if (mRunningTaskTileHidden) {
             setRunningTaskHidden(mRunningTaskTileHidden);
@@ -901,6 +978,7 @@
         for (int i = 0; i < taskCount; i++) {
             getTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
         }
+
         // Fade out the actions view quickly (0.1 range)
         mActionsView.getFullscreenAlpha().setValue(
                 mapToRange(fullscreenProgress, 0, 0.1f, 1f, 0f, LINEAR));
@@ -934,6 +1012,23 @@
         setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top,
                 dp.widthPx - mInsets.right - mTempRect.right,
                 dp.heightPx - mInsets.bottom - mTempRect.bottom);
+        // Force TaskView to update size from thumbnail
+        updateTaskSize();
+    }
+
+    /**
+     * Updates TaskView scaling and translation required to support variable width.
+     */
+    private void updateTaskSize() {
+        float accumulatedTranslationX = 0;
+        final int taskCount = getTaskViewCount();
+        for (int i = 0; i < taskCount; i++) {
+            TaskView taskView = getTaskViewAt(i);
+            taskView.updateTaskSize();
+            taskView.setAccumulatedFullscreenTranslationX(accumulatedTranslationX);
+            accumulatedTranslationX += taskView.getFullscreenTranslationX();
+        }
+        updateGridProperties();
     }
 
     public void getTaskSize(Rect outRect) {
@@ -942,6 +1037,11 @@
         mLastComputedTaskSize.set(outRect);
     }
 
+    /** Gets the last computed task size */
+    public Rect getLastComputedTaskSize() {
+        return mLastComputedTaskSize;
+    }
+
     /** Gets the task size for modal state. */
     public void getModalTaskSize(Rect outRect) {
         mSizeStrategy.calculateModalTaskSize(mActivity, mActivity.getDeviceProfile(), outRect);
@@ -966,7 +1066,7 @@
         mModel.getThumbnailCache().getHighResLoadingState().setFlingingFast(isFlingingFast);
 
         mLiveTileTaskViewSimulator.setScroll(getScrollOffset());
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
+        if (LIVE_TILE.get() && mEnableDrawingLiveTile
                 && mLiveTileParams.getTargetSet() != null) {
             redrawLiveTile();
         }
@@ -987,12 +1087,37 @@
         final int pageCount = getPageCount();
         for (int i = 0; i < pageCount; i++) {
             View page = getPageAt(i);
-            mScrollState.updateInterpolation(
+            mScrollState.updateInterpolation(mActivity.getDeviceProfile(),
                     mOrientationHandler.getChildStartWithTranslation(page));
             ((PageCallbacks) page).onPageScroll(mScrollState);
         }
     }
 
+    @Override
+    protected int getDestinationPage(int scaledScroll) {
+        if (mGridProgress == 0) {
+            return super.getDestinationPage(scaledScroll);
+        }
+
+        final int childCount = getChildCount();
+        if (mPageScrolls == null || childCount != mPageScrolls.length) {
+            return -1;
+        }
+
+        // When in grid, return the page which scroll is closest to screenStart instead of page
+        // nearest to center of screen.
+        int minDistanceFromScreenStart = Integer.MAX_VALUE;
+        int minDistanceFromScreenStartIndex = -1;
+        for (int i = 0; i < childCount; ++i) {
+            int distanceFromScreenStart = Math.abs(mPageScrolls[i] - scaledScroll);
+            if (distanceFromScreenStart < minDistanceFromScreenStart) {
+                minDistanceFromScreenStart = distanceFromScreenStart;
+                minDistanceFromScreenStartIndex = i;
+            }
+        }
+        return minDistanceFromScreenStartIndex;
+    }
+
     /**
      * Iterates through all the tasks, and loads the associated task data for newly visible tasks,
      * and unloads the associated task data for tasks that are no longer visible.
@@ -1004,17 +1129,35 @@
             return;
         }
 
-        int centerPageIndex = getPageNearestToCenterOfScreen();
-        int numChildren = getChildCount();
-        int lower = Math.max(0, centerPageIndex - 2);
-        int upper = Math.min(centerPageIndex + 2, numChildren - 1);
+        int lower = 0;
+        int upper = 0;
+        int visibleStart = 0;
+        int visibleEnd = 0;
+        if (mShowAsGrid) {
+            int screenStart = mOrientationHandler.getPrimaryScroll(this);
+            int pageOrientedSize = mOrientationHandler.getMeasuredSize(this);
+            int halfScreenSize = pageOrientedSize / 2;
+            // Use +/- 50% screen width as visible area.
+            visibleStart = screenStart - halfScreenSize;
+            visibleEnd = screenStart + pageOrientedSize + halfScreenSize;
+        } else {
+            int centerPageIndex = getPageNearestToCenterOfScreen();
+            int numChildren = getChildCount();
+            lower = Math.max(0, centerPageIndex - 2);
+            upper = Math.min(centerPageIndex + 2, numChildren - 1);
+        }
 
         // Update the task data for the in/visible children
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView taskView = getTaskViewAt(i);
             Task task = taskView.getTask();
             int index = indexOfChild(taskView);
-            boolean visible = lower <= index && index <= upper;
+            boolean visible;
+            if (mShowAsGrid) {
+                visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd);
+            } else {
+                visible = lower <= index && index <= upper;
+            }
             if (visible) {
                 if (task == mTmpRunningTask) {
                     // Skip loading if this is the task that we are animating into
@@ -1089,6 +1232,10 @@
         }
     }
 
+    public int getRunningTaskId() {
+        return mRunningTaskId;
+    }
+
     public @Nullable TaskView getRunningTaskView() {
         return getTaskView(mRunningTaskId);
     }
@@ -1141,10 +1288,7 @@
      */
     public void onSwipeUpAnimationSuccess() {
         if (getRunningTaskView() != null) {
-            float startProgress = ENABLE_QUICKSTEP_LIVE_TILE.get() && mLiveTileOverlayAttached
-                    ? LiveTileOverlay.INSTANCE.cancelIconAnimation()
-                    : 0f;
-            animateUpRunningTaskIconScale(startProgress);
+            animateUpRunningTaskIconScale(0f);
         }
         setSwipeDownShouldLaunchApp(true);
     }
@@ -1203,12 +1347,16 @@
         setOnScrollChangeListener(null);
         setEnableFreeScroll(true);
         setEnableDrawingLiveTile(true);
-        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (!LIVE_TILE.get()) {
             setRunningTaskViewShowScreenshot(true);
         }
         setRunningTaskHidden(false);
         animateUpRunningTaskIconScale();
-        animateActionsViewIn();
+
+        // TODO: This should be tied to whether there is a focus app on overview.
+        if (!mShowAsGrid) {
+            animateActionsViewIn();
+        }
     }
 
     /**
@@ -1250,6 +1398,8 @@
         setCurrentPage(getRunningTaskIndex());
         setRunningTaskViewShowScreenshot(false);
         setRunningTaskHidden(runningTaskTileHidden);
+        // Update task size after setting current task.
+        updateTaskSize();
 
         // Reload the task list
         mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
@@ -1289,7 +1439,7 @@
     }
 
     private void setRunningTaskViewShowScreenshot(boolean showScreenshot) {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             TaskView runningTaskView = getRunningTaskView();
             if (runningTaskView != null) {
                 runningTaskView.setShowScreenshot(showScreenshot);
@@ -1358,6 +1508,166 @@
         }
     }
 
+    /**
+     * Updates TaskView and ClearAllButton scaling and translation required to turn into grid
+     * layout.
+     * This method only calculates the potential position and depends on {@link #setGridProgress} to
+     * apply the actual scaling and translation.
+     */
+    private void updateGridProperties() {
+        int taskCount = getTaskViewCount();
+        if (taskCount == 0) {
+            return;
+        }
+
+        final int boxLength = Math.max(mTaskWidth, mTaskHeight);
+
+        float availableHeight =
+                mTaskTopMargin + mTaskHeight + mSizeStrategy.getOverviewActionsHeight(mContext);
+        float rowHeight = (availableHeight - mRowSpacing) / 2;
+        float gridScale = rowHeight / (boxLength + mTaskTopMargin);
+
+        TaskView firstTask = getTaskViewAt(0);
+        float firstTaskWidthOffset;
+        if (mIsRtl) {
+            // Move the first task to the right edge.
+            firstTaskWidthOffset = mTaskWidth - firstTask.getLayoutParams().width * gridScale;
+        } else {
+            // Move the first task to the left edge.
+            firstTaskWidthOffset = -firstTask.getLayoutParams().width * (1 - gridScale);
+        }
+
+        int topRowWidth = 0;
+        int bottomRowWidth = 0;
+        float topAccumulatedTranslationX = 0;
+        float bottomAccumulatedTranslationX = 0;
+        IntSet topSet = new IntSet();
+        float[] gridTranslations = new float[taskCount];
+        for (int i = 0; i < taskCount; i++) {
+            TaskView taskView = getTaskViewAt(i);
+            taskView.setGridScale(gridScale);
+
+            float taskWidthDiff = mTaskWidth - taskView.getLayoutParams().width * gridScale;
+            float taskWidthOffset = mIsRtl ? taskWidthDiff : -taskWidthDiff;
+            // Visually we want to move all task by firstTaskWidthOffset, but calculate page scroll
+            // according to right edge (or left in nonRtl) of TaskView.
+            gridTranslations[i] = firstTaskWidthOffset - taskWidthOffset;
+            taskView.setGridOffsetTranslationX(taskWidthOffset);
+
+            // Off-set gap due to task scaling.
+            float widthDiff = taskView.getLayoutParams().width * (1 - gridScale);
+            float gridScaleTranslationX = mIsRtl ? widthDiff : -widthDiff;
+            gridTranslations[i] += gridScaleTranslationX;
+
+            // Visible offset caused by having scaling pivot on top-right.
+            taskView.setNonRtlVisibleOffset(mIsRtl ? 0 : widthDiff);
+
+            if (topRowWidth <= bottomRowWidth) {
+                gridTranslations[i] += topAccumulatedTranslationX;
+                topRowWidth += taskView.getLayoutParams().width * gridScale + mPageSpacing;
+                topSet.add(i);
+
+                taskView.setGridTranslationY(0);
+
+                // Move horizontally into empty space.
+                float widthOffset = 0;
+                for (int j = i - 1; !topSet.contains(j) && j >= 0; j--) {
+                    widthOffset += getTaskViewAt(j).getLayoutParams().width * gridScale
+                            + mPageSpacing;
+                }
+
+                float gridTranslationX = mIsRtl ? widthOffset : -widthOffset;
+                gridTranslations[i] += gridTranslationX;
+                topAccumulatedTranslationX += gridTranslationX + gridScaleTranslationX;
+                bottomAccumulatedTranslationX += gridScaleTranslationX;
+            } else {
+                gridTranslations[i] += bottomAccumulatedTranslationX;
+                bottomRowWidth += taskView.getLayoutParams().width * gridScale + mPageSpacing;
+
+                // Move into bottom row.
+                float heightOffset = (boxLength + mTaskTopMargin) * gridScale + mRowSpacing;
+                taskView.setGridTranslationY(heightOffset);
+
+                // Move horizontally into empty space.
+                float widthOffset = 0;
+                for (int j = i - 1; topSet.contains(j); j--) {
+                    widthOffset += getTaskViewAt(j).getLayoutParams().width * gridScale
+                            + mPageSpacing;
+                }
+
+                float gridTranslationX = mIsRtl ? widthOffset : -widthOffset;
+                gridTranslations[i] += gridTranslationX;
+                topAccumulatedTranslationX += gridScaleTranslationX;
+                bottomAccumulatedTranslationX += gridTranslationX + gridScaleTranslationX;
+            }
+        }
+
+        // Use the accumulated translation of the longer row.
+        float clearAllAccumulatedTranslation = mIsRtl ? Math.max(topAccumulatedTranslationX,
+                bottomAccumulatedTranslationX) : Math.min(topAccumulatedTranslationX,
+                bottomAccumulatedTranslationX);
+
+        // If the last task is on the shorter row, ClearAllButton will embed into the shorter row
+        // which is not what we want. Compensate the width difference of the 2 rows in that case.
+        float shorterRowCompensation = 0;
+        if (topRowWidth <= bottomRowWidth) {
+            if (topSet.contains(taskCount - 1)) {
+                shorterRowCompensation = bottomRowWidth - topRowWidth;
+            }
+        } else {
+            if (!topSet.contains(taskCount - 1)) {
+                shorterRowCompensation = topRowWidth - bottomRowWidth;
+            }
+        }
+        float clearAllShorterRowCompensation =
+                mIsRtl ? -shorterRowCompensation : shorterRowCompensation;
+
+        // If the total width is shorter than one task's width, move ClearAllButton further away
+        // accordingly.
+        float clearAllShortTotalCompensation = 0;
+        float longRowWidth = Math.max(topRowWidth, bottomRowWidth);
+        if (longRowWidth < mTaskWidth) {
+            float shortTotalCompensation = mTaskWidth - longRowWidth;
+            clearAllShortTotalCompensation =
+                    mIsRtl ? -shortTotalCompensation : shortTotalCompensation;
+        }
+
+        float clearAllTotalTranslationX = firstTaskWidthOffset + clearAllAccumulatedTranslation
+                + clearAllShorterRowCompensation + clearAllShortTotalCompensation;
+
+        // We need to maintain first task's grid translation at 0, now shift translation of all
+        // the TaskViews to achieve that.
+        for (int i = 0; i < taskCount; i++) {
+            getTaskViewAt(i).setGridTranslationX(gridTranslations[i] - gridTranslations[0]);
+        }
+        mClearAllButton.setGridTranslationPrimary(clearAllTotalTranslationX - gridTranslations[0]);
+
+        setGridProgress(mGridProgress);
+    }
+
+    /**
+     * Moves TaskView and ClearAllButton between carousel and 2 row grid.
+     *
+     * @param gridProgress 0 = carousel; 1 = 2 row grid.
+     */
+    public void setGridProgress(float gridProgress) {
+        int taskCount = getTaskViewCount();
+        if (taskCount == 0) {
+            return;
+        }
+
+        if (!mShowAsGrid) {
+            gridProgress = 0;
+        }
+
+        mGridProgress = gridProgress;
+
+        for (int i = 0; i < taskCount; i++) {
+            getTaskViewAt(i).setGridProgress(gridProgress);
+        }
+        mClearAllButton.setGridProgress(gridProgress);
+    }
+
     private void enableLayoutTransitions() {
         if (mLayoutTransition == null) {
             mLayoutTransition = new LayoutTransition();
@@ -1422,13 +1732,13 @@
         /**
          * Updates linearInterpolation for the provided child position
          */
-        public void updateInterpolation(float childStart) {
+        public void updateInterpolation(DeviceProfile deviceProfile, float childStart) {
             float pageCenter = childStart + halfPageSize;
             float distanceFromScreenCenter = screenCenter - pageCenter;
             // How far the page has to move from the center to be offscreen, taking into account
             // the EDGE_SCALE_DOWN_FACTOR that will be applied at that position.
             float distanceToReachEdge = halfScreenSize
-                    + halfPageSize * (1 - TaskView.EDGE_SCALE_DOWN_FACTOR);
+                    + halfPageSize * (1 - TaskView.getEdgeScaleDownFactor(deviceProfile));
             linearInterpolation = Math.min(1,
                     Math.abs(distanceFromScreenCenter) / distanceToReachEdge);
         }
@@ -1444,12 +1754,13 @@
         }
     }
 
-    private void addDismissedTaskAnimations(View taskView, long duration, PendingAnimation anim) {
+    private void addDismissedTaskAnimations(TaskView taskView, long duration,
+            PendingAnimation anim) {
         // Use setFloat instead of setViewAlpha as we want to keep the view visible even when it's
         // alpha is set to 0 so that it can be recycled in the view pool properly
         anim.setFloat(taskView, VIEW_ALPHA, 0, ACCEL_2);
-        FloatProperty<View> secondaryViewTranslate =
-            mOrientationHandler.getSecondaryViewTranslate();
+        FloatProperty<TaskView> secondaryViewTranslate =
+                taskView.getDismissTaskTranslationProperty();
         int secondaryTaskDimension = mOrientationHandler.getSecondaryDimension(taskView);
         int verticalFactor = mOrientationHandler.getSecondaryTranslationDirectionFactor();
 
@@ -1492,7 +1803,7 @@
                 if (animateTaskView) {
                     addDismissedTaskAnimations(taskView, duration, anim);
                 }
-            } else {
+            } else if (!mShowAsGrid) {  // Don't animate other tasks when dismissing in grid for now
                 // If we just take newScroll - oldScroll, everything to the right of dragged task
                 // translates to the left. We need to offset this in some cases:
                 // - In RTL, add page offset to all pages, since we want pages to move to the right
@@ -1515,7 +1826,7 @@
                 int scrollDiff = newScroll[i] - oldScroll[i] + offset;
                 if (scrollDiff != 0) {
                     FloatProperty translationProperty = child instanceof TaskView
-                            ? ((TaskView) child).getPrimaryFillDismissGapTranslationProperty()
+                            ? ((TaskView) child).getFillDismissGapTranslationProperty()
                             : mOrientationHandler.getPrimaryViewTranslate();
 
                     ResourceProvider rp = DynamicResource.provider(mActivity);
@@ -1534,7 +1845,7 @@
             anim.addOnFrameCallback(this::updateCurveProperties);
         }
 
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && getRunningTaskView() == taskView) {
+        if (LIVE_TILE.get() && getRunningTaskView() == taskView) {
             anim.addOnFrameCallback(() -> {
                 mLiveTileTaskViewSimulator.taskSecondaryTranslation.value =
                         mOrientationHandler.getSecondaryValue(
@@ -1553,7 +1864,7 @@
         mPendingAnimation.addEndListener(new Consumer<Boolean>() {
             @Override
             public void accept(Boolean success) {
-                if (ENABLE_QUICKSTEP_LIVE_TILE.get() && taskView.isRunningTask() && success) {
+                if (LIVE_TILE.get() && taskView.isRunningTask() && success) {
                     finishRecentsAnimation(true /* toHome */, () -> onEnd(success));
                 } else {
                     onEnd(success);
@@ -1585,6 +1896,8 @@
                         startHome();
                     } else {
                         snapToPageImmediately(pageToSnapTo);
+                        // Grid got messed up, reapply.
+                        updateGridProperties();
                     }
                     // Update the layout synchronously so that the position of next view is
                     // immediately available.
@@ -1729,7 +2042,7 @@
         if (alpha > 0) {
             setVisibility(VISIBLE);
         } else if (!mFreezeViewVisibility) {
-            setVisibility(GONE);
+            setVisibility(INVISIBLE);
         }
     }
 
@@ -1741,7 +2054,7 @@
         if (mFreezeViewVisibility != freezeViewVisibility) {
             mFreezeViewVisibility = freezeViewVisibility;
             if (!mFreezeViewVisibility) {
-                setVisibility(mContentAlpha > 0 ? VISIBLE : GONE);
+                setVisibility(mContentAlpha > 0 ? VISIBLE : INVISIBLE);
             }
         }
     }
@@ -1968,10 +2281,6 @@
             centerToOffscreenProgress = Utilities.mapRange(centerToOffscreenProgress,
                     distanceFromMidpoint / distanceToOffscreen, 1);
         }
-        // Find the task's scale based on its offscreen progress, then see how far it still needs to
-        // move to be completely offscreen.
-        Utilities.scaleRectFAboutCenter(taskPosition,
-                TaskView.getCurveScaleForInterpolation(centerToOffscreenProgress));
         distanceToOffscreen = desiredLeft - taskPosition.left;
         // Finally, we need to account for RecentsView scale, because it moves tasks based on its
         // pivot. To do this, we move the task position to where it would be offscreen at scale = 1
@@ -1990,7 +2299,7 @@
         mTaskViewsSecondaryTranslation = translation;
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView task = getTaskViewAt(i);
-            mOrientationHandler.getSecondaryViewTranslate().set(task, translation / getScaleY());
+            task.getTaskResistanceTranslationProperty().set(task, translation / getScaleY());
         }
         mLiveTileTaskViewSimulator.recentsViewSecondaryTranslation.value = translation;
     }
@@ -2097,12 +2406,12 @@
             anim.play(ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS, 1));
         } else {
             // We are launching an adjacent task, so parallax the center and other adjacent task.
-            float displacementX = tv.getWidth() * (toScale - tv.getCurveScale());
+            float displacementX = tv.getWidth() * (toScale - 1f);
             float primaryTranslation = mIsRtl ? -displacementX : displacementX;
             anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex),
                     mOrientationHandler.getPrimaryViewTranslate(), primaryTranslation));
             int runningTaskIndex = recentsView.getRunningTaskIndex();
-            if (ENABLE_QUICKSTEP_LIVE_TILE.get() && runningTaskIndex != -1
+            if (LIVE_TILE.get() && runningTaskIndex != -1
                     && runningTaskIndex != taskIndex) {
                 anim.play(ObjectAnimator.ofFloat(
                         recentsView.getLiveTileTaskViewSimulator().taskPrimaryTranslation,
@@ -2179,7 +2488,7 @@
 
         mPendingAnimation = new PendingAnimation(duration);
         mPendingAnimation.add(anim);
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             mLiveTileTaskViewSimulator.addOverviewToAppAnim(mPendingAnimation, interpolator);
             mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
         }
@@ -2191,7 +2500,7 @@
                         tv.notifyTaskLaunchFailed(TAG);
                     }
                 };
-                if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+                if (LIVE_TILE.get()) {
                     finishRecentsAnimation(false /* toRecents */, null);
                     onLaunchResult.accept(true /* success */);
                 } else {
@@ -2277,7 +2586,9 @@
     }
 
     public void redrawLiveTile() {
-        mLiveTileTaskViewSimulator.apply(mLiveTileParams);
+        if (mLiveTileParams.getTargetSet() != null) {
+            mLiveTileTaskViewSimulator.apply(mLiveTileParams);
+        }
     }
 
     public TaskViewSimulator getLiveTileTaskViewSimulator() {
@@ -2299,16 +2610,6 @@
         }
     }
 
-    public void setLiveTileOverlayAttached(boolean liveTileOverlayAttached) {
-        mLiveTileOverlayAttached = liveTileOverlayAttached;
-    }
-
-    public void updateLiveTileIcon(Drawable icon) {
-        if (mLiveTileOverlayAttached) {
-            LiveTileOverlay.INSTANCE.setIcon(icon);
-        }
-    }
-
     public void finishRecentsAnimation(boolean toRecents, Runnable onFinishComplete) {
         if (mRecentsAnimationController == null) {
             if (onFinishComplete != null) {
@@ -2357,6 +2658,54 @@
     }
 
     @Override
+    protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren,
+            ComputePageScrollsLogic scrollLogic) {
+        boolean pageScrollChanged = super.getPageScrolls(outPageScrolls, layoutChildren,
+                scrollLogic);
+
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            float scrollDiff = 0;
+            if (child instanceof TaskView) {
+                scrollDiff = ((TaskView) child).getScrollAdjustment(mOverviewFullscreenEnabled,
+                        mOverviewGridEnabled);
+            } else if (child instanceof ClearAllButton) {
+                scrollDiff = ((ClearAllButton) child).getScrollAdjustment(mOverviewGridEnabled);
+            }
+
+            if (scrollDiff != 0) {
+                outPageScrolls[i] += scrollDiff;
+                pageScrollChanged = true;
+            }
+        }
+        return pageScrollChanged;
+    }
+
+    @Override
+    protected int getChildOffset(int index) {
+        int childOffset = super.getChildOffset(index);
+        View child = getChildAt(index);
+        if (child instanceof TaskView) {
+            childOffset += ((TaskView) child).getOffsetAdjustment(mOverviewFullscreenEnabled,
+                    mOverviewGridEnabled);
+        } else if (child instanceof ClearAllButton) {
+            childOffset += ((ClearAllButton) child).getOffsetAdjustment(mOverviewGridEnabled);
+        }
+        return childOffset;
+    }
+
+    @Override
+    protected int getChildVisibleSize(int index) {
+        final TaskView taskView = getTaskViewAtByAbsoluteIndex(index);
+        if (taskView == null) {
+            return super.getChildVisibleSize(index);
+        }
+        return (int) (super.getChildVisibleSize(index) * taskView.getSizeAdjustment(
+                mOverviewFullscreenEnabled, mOverviewGridEnabled));
+    }
+
+    @Override
     protected int computeMaxScroll() {
         if (getTaskViewCount() > 0) {
             if (mDisallowScrollToClearAll) {
@@ -2394,7 +2743,7 @@
     }
 
     /**
-     * @return How many pixels the page is offset on the currently laid out dominant axis.
+     * Returns how many pixels the page is offset on the currently laid out dominant axis.
      */
     public int getScrollOffset(int pageIndex) {
         if (pageIndex == -1) {
@@ -2410,6 +2759,20 @@
         return getScrollForPage(pageIndex) - scroll;
     }
 
+    /**
+     * Returns how many pixels the task is offset on the currently laid out secondary axis
+     * according to {@link #mGridProgress}.
+     */
+    public float getGridTranslationSecondary(int pageIndex) {
+        TaskView taskView = getTaskViewAtByAbsoluteIndex(pageIndex);
+        if (taskView == null) {
+            return 0;
+        }
+
+        return mOrientationHandler.getSecondaryValue(taskView.getGridTranslationX(),
+                taskView.getGridTranslationY());
+    }
+
     public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
         float degreesRotated;
         if (navbarRotation == 0) {
@@ -2454,6 +2817,23 @@
         }
     }
 
+    public void setOverviewGridEnabled(boolean overviewGridEnabled) {
+        if (mOverviewGridEnabled != overviewGridEnabled) {
+            mOverviewGridEnabled = overviewGridEnabled;
+            // Request layout to ensure scroll position is recalculated with updated mGridProgress.
+            requestLayout();
+        }
+    }
+
+    public void setOverviewFullscreenEnabled(boolean overviewFullscreenEnabled) {
+        if (mOverviewFullscreenEnabled != overviewFullscreenEnabled) {
+            mOverviewFullscreenEnabled = overviewFullscreenEnabled;
+            // Request layout to ensure scroll position is recalculated with updated
+            // mFullscreenProgress.
+            requestLayout();
+        }
+    }
+
     /**
      * Switch the current running task view to static snapshot mode,
      * capturing the snapshot at the same time.
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index 4aff7e3..2315147 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -16,7 +16,7 @@
 
 package com.android.quickstep.views;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA;
 
 import android.animation.Animator;
@@ -24,7 +24,6 @@
 import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.MotionEvent;
@@ -35,18 +34,17 @@
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
-import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskUtils;
-import com.android.quickstep.views.IconView.OnScaleUpdateListener;
 
 /**
  * Contains options for a recent task when long-pressing its icon.
@@ -55,42 +53,15 @@
 
     private static final Rect sTempRect = new Rect();
 
-    private final OnScaleUpdateListener mTaskViewIconScaleListener = new OnScaleUpdateListener() {
-        @Override
-        public void onScaleUpdate(float scale) {
-            final Drawable drawable = mTaskIcon.getDrawable();
-            if (drawable instanceof FastBitmapDrawable) {
-                if (scale != ((FastBitmapDrawable) drawable).getScale()) {
-                    mMenuIconDrawable.setScale(scale);
-                }
-            }
-        }
-    };
-
-    private final OnScaleUpdateListener mMenuIconScaleListener = new OnScaleUpdateListener() {
-        @Override
-        public void onScaleUpdate(float scale) {
-            final Drawable taskViewDrawable = mTaskView.getIconView().getDrawable();
-            if (taskViewDrawable instanceof FastBitmapDrawable) {
-                final float currentScale = ((FastBitmapDrawable) taskViewDrawable).getScale();
-                if (currentScale != scale) {
-                    ((FastBitmapDrawable) taskViewDrawable).setScale(scale);
-                }
-            }
-        }
-    };
-
     private static final int REVEAL_OPEN_DURATION = 150;
     private static final int REVEAL_CLOSE_DURATION = 100;
 
     private final float mThumbnailTopMargin;
     private BaseDraggingActivity mActivity;
     private TextView mTaskName;
-    private IconView mTaskIcon;
     private AnimatorSet mOpenCloseAnimator;
     private TaskView mTaskView;
     private LinearLayout mOptionLayout;
-    private FastBitmapDrawable mMenuIconDrawable;
 
     public TaskMenuView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
@@ -107,7 +78,6 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
         mTaskName = findViewById(R.id.task_name);
-        mTaskIcon = findViewById(R.id.task_icon);
         mOptionLayout = findViewById(R.id.menu_option_layout);
     }
 
@@ -134,15 +104,6 @@
     }
 
     @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-
-        // Remove all scale listeners when menu is removed
-        mTaskView.getIconView().removeUpdateScaleListener(mTaskViewIconScaleListener);
-        mTaskIcon.removeUpdateScaleListener(mMenuIconScaleListener);
-    }
-
-    @Override
     protected boolean isOfType(int type) {
         return (type & TYPE_TASK_MENU) != 0;
     }
@@ -153,7 +114,12 @@
         // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set,
         // which would render the X and Y position set here incorrect
         setPivotX(0);
-        setPivotY(0);
+        if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+            // In tablet, set pivotY to original position without mThumbnailTopMargin adjustment.
+            setPivotY(-mThumbnailTopMargin);
+        } else {
+            setPivotY(0);
+        }
         setRotation(pagedOrientationHandler.getDegreesRotated());
         setX(pagedOrientationHandler.getTaskMenuX(x, mTaskView.getThumbnail()));
         setY(pagedOrientationHandler.getTaskMenuY(adjustedY, mTaskView.getThumbnail()));
@@ -203,23 +169,9 @@
     }
 
     private void addMenuOptions(TaskView taskView) {
-        Drawable icon = taskView.getTask().icon.getConstantState().newDrawable();
-        mTaskIcon.setDrawable(icon);
-        mTaskIcon.setOnClickListener(v -> close(true));
         mTaskName.setText(TaskUtils.getTitle(getContext(), taskView.getTask()));
         mTaskName.setOnClickListener(v -> close(true));
 
-        // Set the icons to match scale by listening to each other's changes
-        mMenuIconDrawable = icon instanceof FastBitmapDrawable ? (FastBitmapDrawable) icon : null;
-        taskView.getIconView().addUpdateScaleListener(mTaskViewIconScaleListener);
-        mTaskIcon.addUpdateScaleListener(mMenuIconScaleListener);
-
-        // Move the icon and text up half an icon size to lay over the TaskView
-        LinearLayout.LayoutParams params =
-                (LinearLayout.LayoutParams) mTaskIcon.getLayoutParams();
-        params.topMargin = (int) -mThumbnailTopMargin;
-        mTaskIcon.setLayoutParams(params);
-
         TaskOverlayFactory.getEnabledShortcuts(taskView).forEach(this::addMenuOption);
     }
 
@@ -231,7 +183,7 @@
         LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams();
         mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp);
         menuOptionView.setOnClickListener(view -> {
-            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            if (LIVE_TILE.get()) {
                 RecentsView recentsView = mTaskView.getRecentsView();
                 recentsView.switchToScreenshot(null,
                         () -> recentsView.finishRecentsAnimation(true /* toRecents */,
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index 445e490..4c21745 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -19,7 +19,7 @@
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
 
 import android.content.Context;
@@ -51,6 +51,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SystemUiController;
@@ -318,7 +319,7 @@
 
     public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height,
             float cornerRadius) {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) {
                 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint);
                 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius,
@@ -480,9 +481,11 @@
             float scale = thumbnailData.scale;
             final float thumbnailScale;
 
-            // Landscape vs portrait change
+            // Landscape vs portrait change.
+            // Note: Disable rotation in grid layout.
             boolean windowingModeSupportsRotation = !dp.isMultiWindowMode
-                    && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
+                    && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN
+                    && !(dp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get());
             isOrientationDifferent = isOrientationChange(deltaRotate)
                     && windowingModeSupportsRotation;
             if (canvasWidth == 0 || canvasHeight == 0 || scale == 0) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index e8f590f..88545c6 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -30,19 +30,20 @@
 import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
 import static com.android.launcher3.Utilities.comp;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static com.android.launcher3.anim.Interpolators.EXAGGERATED_EASE;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
@@ -50,8 +51,6 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.InsetDrawable;
 import android.os.Bundle;
 import android.os.Handler;
 import android.util.AttributeSet;
@@ -61,6 +60,7 @@
 import android.view.Surface;
 import android.view.TouchDelegate;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.ViewOutlineProvider;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
@@ -72,6 +72,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.statemanager.StatefulActivity;
@@ -110,20 +111,14 @@
 
     private static final String TAG = TaskView.class.getSimpleName();
 
-    /** A curve of x from 0 to 1, where 0 is the center of the screen and 1 is the edge. */
-    private static final TimeInterpolator CURVE_INTERPOLATOR
-            = x -> (float) -Math.cos(x * Math.PI) / 2f + .5f;
-
     /**
      * The alpha of a black scrim on a page in the carousel as it leaves the screen.
      * In the resting position of the carousel, the adjacent pages have about half this scrim.
      */
     public static final float MAX_PAGE_SCRIM_ALPHA = 0.4f;
 
-    /**
-     * How much to scale down pages near the edge of the screen.
-     */
-    public static final float EDGE_SCALE_DOWN_FACTOR = 0.03f;
+    private static final float EDGE_SCALE_DOWN_FACTOR_CAROUSEL = 0.03f;
+    private static final float EDGE_SCALE_DOWN_FACTOR_GRID = 0.00f;
 
     public static final long SCALE_ICON_DURATION = 120;
     private static final long DIM_ANIM_DURATION = 700;
@@ -152,29 +147,29 @@
                 }
             };
 
-    private static final FloatProperty<TaskView> FILL_DISMISS_GAP_TRANSLATION_X =
-            new FloatProperty<TaskView>("fillDismissGapTranslationX") {
+    private static final FloatProperty<TaskView> DISMISS_TRANSLATION_X =
+            new FloatProperty<TaskView>("dismissTranslationX") {
                 @Override
                 public void setValue(TaskView taskView, float v) {
-                    taskView.setFillDismissGapTranslationX(v);
+                    taskView.setDismissTranslationX(v);
                 }
 
                 @Override
                 public Float get(TaskView taskView) {
-                    return taskView.mFillDismissGapTranslationX;
+                    return taskView.mDismissTranslationX;
                 }
             };
 
-    private static final FloatProperty<TaskView> FILL_DISMISS_GAP_TRANSLATION_Y =
-            new FloatProperty<TaskView>("fillDismissGapTranslationY") {
+    private static final FloatProperty<TaskView> DISMISS_TRANSLATION_Y =
+            new FloatProperty<TaskView>("dismissTranslationY") {
                 @Override
                 public void setValue(TaskView taskView, float v) {
-                    taskView.setFillDismissGapTranslationY(v);
+                    taskView.setDismissTranslationY(v);
                 }
 
                 @Override
                 public Float get(TaskView taskView) {
-                    return taskView.mFillDismissGapTranslationY;
+                    return taskView.mDismissTranslationY;
                 }
             };
 
@@ -204,6 +199,32 @@
                 }
             };
 
+    private static final FloatProperty<TaskView> TASK_RESISTANCE_TRANSLATION_X =
+            new FloatProperty<TaskView>("taskResistanceTranslationX") {
+                @Override
+                public void setValue(TaskView taskView, float v) {
+                    taskView.setTaskResistanceTranslationX(v);
+                }
+
+                @Override
+                public Float get(TaskView taskView) {
+                    return taskView.mTaskResistanceTranslationX;
+                }
+            };
+
+    private static final FloatProperty<TaskView> TASK_RESISTANCE_TRANSLATION_Y =
+            new FloatProperty<TaskView>("taskResistanceTranslationY") {
+                @Override
+                public void setValue(TaskView taskView, float v) {
+                    taskView.setTaskResistanceTranslationY(v);
+                }
+
+                @Override
+                public Float get(TaskView taskView) {
+                    return taskView.mTaskResistanceTranslationY;
+                }
+            };
+
     private final OnAttachStateChangeListener mTaskMenuStateListener =
             new OnAttachStateChangeListener() {
                 @Override
@@ -226,17 +247,30 @@
     private TaskMenuView mMenuView;
     private IconView mIconView;
     private final DigitalWellBeingToast mDigitalWellBeingToast;
-    private float mCurveScale;
     private float mFullscreenProgress;
+    private float mGridProgress;
+    private float mFullscreenScale = 1;
+    private float mGridScale = 1;
     private final FullscreenDrawParams mCurrentFullscreenParams;
     private final StatefulActivity mActivity;
 
     // Various causes of changing primary translation, which we aggregate to setTranslationX/Y().
-    // TODO: We should do this for secondary translation properties as well.
-    private float mFillDismissGapTranslationX;
-    private float mFillDismissGapTranslationY;
+    private float mDismissTranslationX;
+    private float mDismissTranslationY;
     private float mTaskOffsetTranslationX;
     private float mTaskOffsetTranslationY;
+    private float mTaskResistanceTranslationX;
+    private float mTaskResistanceTranslationY;
+    // The following translation variables should only be used in the same orientation as Launcher.
+    private float mFullscreenTranslationX;
+    private float mAccumulatedFullscreenTranslationX;
+    private float mBoxTranslationY;
+    // The following grid translations scales with mGridProgress.
+    private float mGridTranslationX;
+    private float mGridTranslationY;
+    // Offset translation does not affect scroll calculation.
+    private float mGridOffsetTranslationX;
+    private float mNonRtlVisibleOffset;
 
     private ObjectAnimator mIconAndDimAnimator;
     private float mIconScaleAnimStartProgress = 0;
@@ -274,7 +308,7 @@
             if (getTask() == null) {
                 return;
             }
-            if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) {
+            if (LIVE_TILE.get() && isRunningTask()) {
                 if (!mIsClickableAsLiveTile) {
                     return;
                 }
@@ -356,10 +390,6 @@
         return false;
     }
 
-    public void setIsClickableAsLiveTile(boolean isClickableAsLiveTile) {
-        mIsClickableAsLiveTile = isClickableAsLiveTile;
-    }
-
     private void computeAndSetIconTouchDelegate() {
         float iconHalfSize = mIconView.getWidth() / 2f;
         mIconCenterCoords[0] = mIconCenterCoords[1] = iconHalfSize;
@@ -405,10 +435,9 @@
             mContextualChip.setScaleX(comp(modalness));
             mContextualChip.setScaleY(comp(modalness));
         }
-        if (mContextualChipWrapper != null) {
-            mContextualChipWrapper.setAlpha(comp(modalness));
-        }
-        mDigitalWellBeingToast.updateBannerOffset(modalness);
+        mDigitalWellBeingToast.updateBannerOffset(modalness,
+                mCurrentFullscreenParams.mCurrentDrawnInsets.top
+                        + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom);
     }
 
     public TaskMenuView getMenuView() {
@@ -436,6 +465,10 @@
         return mTask;
     }
 
+    public boolean hasTaskId(int taskId) {
+        return mTask != null && mTask.key != null && mTask.key.id == taskId;
+    }
+
     public TaskThumbnailView getThumbnail() {
         return mSnapshotView;
     }
@@ -526,9 +559,6 @@
             mIconLoadRequest = iconCache.updateIconInBackground(mTask,
                     (task) -> {
                         setIcon(task.icon);
-                        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) {
-                            getRecentsView().updateLiveTileIcon(task.icon);
-                        }
                         mDigitalWellBeingToast.initialize(mTask);
                     });
         } else {
@@ -585,6 +615,9 @@
         boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
         LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams();
         int thumbnailPadding = (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin);
+        int taskIconMargin = (int) getResources().getDimension(R.dimen.task_icon_top_margin);
+        int taskIconHeight = (int) getResources().getDimension(R.dimen.task_thumbnail_icon_size);
+        int iconTopMargin = taskIconMargin - taskIconHeight + thumbnailPadding;
         LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
         switch (orientationHandler.getRotation()) {
             case ROTATION_90:
@@ -596,7 +629,8 @@
             case ROTATION_180:
                 iconParams.gravity = BOTTOM | CENTER_HORIZONTAL;
                 iconParams.bottomMargin = -thumbnailPadding;
-                iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
+                iconParams.leftMargin = iconParams.rightMargin = 0;
+                iconParams.topMargin = iconTopMargin;
                 break;
             case ROTATION_270:
                 iconParams.gravity = (isRtl ? END : START) | CENTER_VERTICAL;
@@ -607,7 +641,8 @@
             case Surface.ROTATION_0:
             default:
                 iconParams.gravity = TOP | CENTER_HORIZONTAL;
-                iconParams.leftMargin = iconParams.topMargin = iconParams.rightMargin = 0;
+                iconParams.leftMargin = iconParams.rightMargin = 0;
+                iconParams.topMargin = iconTopMargin;
                 break;
         }
         mIconView.setLayoutParams(iconParams);
@@ -623,7 +658,7 @@
             progress = 1 - progress;
         }
         mFocusTransitionProgress = progress;
-        mSnapshotView.setDimAlphaMultipler(progress);
+        mSnapshotView.setDimAlphaMultipler(0);
         float iconScalePercentage = (float) SCALE_ICON_DURATION / DIM_ANIM_DURATION;
         float lowerClamp = invert ? 1f - iconScalePercentage : 0;
         float upperClamp = invert ? 1 : iconScalePercentage;
@@ -636,7 +671,9 @@
             mContextualChip.setScaleX(scale);
             mContextualChip.setScaleY(scale);
         }
-        mDigitalWellBeingToast.updateBannerOffset(1f - scale);
+        mDigitalWellBeingToast.updateBannerOffset(1f - scale,
+                mCurrentFullscreenParams.mCurrentDrawnInsets.top
+                        + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom);
     }
 
     public void setIconScaleAnimStartProgress(float startProgress) {
@@ -671,11 +708,12 @@
     }
 
     protected void resetViewTransforms() {
-        setCurveScale(1);
-        mFillDismissGapTranslationX = mTaskOffsetTranslationX = 0f;
-        mFillDismissGapTranslationY = mTaskOffsetTranslationY = 0f;
-        setTranslationX(0f);
-        setTranslationY(0f);
+        // fullscreenTranslation and accumulatedTranslation should not be reset, as
+        // resetViewTransforms is called during Quickswitch scrolling.
+        mDismissTranslationX = mTaskOffsetTranslationX = mTaskResistanceTranslationX = 0f;
+        mDismissTranslationY = mTaskOffsetTranslationY = mTaskResistanceTranslationY = 0f;
+        applyTranslationX();
+        applyTranslationY();
         setTranslationZ(0);
         setAlpha(mStableAlpha);
         setIconScaleAndDim(1);
@@ -688,6 +726,9 @@
 
     @Override
     public void onRecycle() {
+        mFullscreenTranslationX = mAccumulatedFullscreenTranslationX = mGridTranslationX =
+                mGridTranslationY =
+                        mGridOffsetTranslationX = mBoxTranslationY = mNonRtlVisibleOffset = 0f;
         resetViewTransforms();
         // Clear any references to the thumbnail (it will be re-read either from the cache or the
         // system on next bind)
@@ -703,13 +744,6 @@
             return;
         }
 
-        float curveInterpolation =
-                CURVE_INTERPOLATOR.getInterpolation(scrollState.linearInterpolation);
-        float curveScaleForCurveInterpolation = getCurveScaleForCurveInterpolation(
-                curveInterpolation);
-        mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA);
-        setCurveScale(curveScaleForCurveInterpolation);
-
         float dwbBannerAlpha = Utilities.boundToRange(1.0f - 2 * scrollState.linearInterpolation,
                 0f, 1f);
         mDigitalWellBeingToast.updateBannerAlpha(dwbBannerAlpha);
@@ -740,30 +774,16 @@
             layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL;
             int expectedChipHeight = getExpectedViewHeight(view);
             float chipOffset = getResources().getDimension(R.dimen.chip_hint_vertical_offset);
-            layoutParams.bottomMargin = (int)
-                    (((MarginLayoutParams) mSnapshotView.getLayoutParams()).bottomMargin
-                            - expectedChipHeight + chipOffset);
+            layoutParams.bottomMargin = -expectedChipHeight - (int) chipOffset;
             mContextualChip = ((FrameLayout) mContextualChipWrapper).getChildAt(0);
             mContextualChip.setScaleX(0f);
             mContextualChip.setScaleY(0f);
-            GradientDrawable scrimDrawable = (GradientDrawable) getResources().getDrawable(
-                    R.drawable.chip_scrim_gradient, mActivity.getTheme());
-            float cornerRadius = getTaskCornerRadius();
-            scrimDrawable.setCornerRadii(
-                    new float[]{0, 0, 0, 0, cornerRadius, cornerRadius, cornerRadius,
-                            cornerRadius});
-            InsetDrawable scrimDrawableInset = new InsetDrawable(scrimDrawable, 0, 0, 0,
-                    (int) (expectedChipHeight - chipOffset));
-            mContextualChipWrapper.setBackground(scrimDrawableInset);
-            mContextualChipWrapper.setPadding(0, 0, 0, 0);
-            mContextualChipWrapper.setAlpha(0f);
             addView(view, getChildCount(), layoutParams);
             if (mContextualChip != null) {
                 mContextualChip.animate().scaleX(1f).scaleY(1f).setDuration(50);
             }
             if (mContextualChipWrapper != null) {
                 mChipTouchDelegate = new TransformingTouchDelegate(mContextualChipWrapper);
-                mContextualChipWrapper.animate().alpha(1f).setDuration(50);
             }
         }
     }
@@ -791,40 +811,69 @@
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
-        setPivotX((right - left) * 0.5f);
-        setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f);
+        if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+            setPivotX(getLayoutDirection() == LAYOUT_DIRECTION_RTL ? (right - left) : 0);
+            setPivotY(mSnapshotView.getTop());
+        } else {
+            setPivotX((right - left) * 0.5f);
+            setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f);
+        }
         if (Utilities.ATLEAST_Q) {
             SYSTEM_GESTURE_EXCLUSION_RECT.get(0).set(0, 0, getWidth(), getHeight());
             setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT);
         }
     }
 
-    public static float getCurveScaleForInterpolation(float linearInterpolation) {
-        float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(linearInterpolation);
-        return getCurveScaleForCurveInterpolation(curveInterpolation);
+    /**
+     * How much to scale down pages near the edge of the screen.
+     */
+    public static float getEdgeScaleDownFactor(DeviceProfile deviceProfile) {
+        if (deviceProfile.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+            return EDGE_SCALE_DOWN_FACTOR_GRID;
+        } else {
+            return EDGE_SCALE_DOWN_FACTOR_CAROUSEL;
+        }
     }
 
-    private static float getCurveScaleForCurveInterpolation(float curveInterpolation) {
-        return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR;
+    private void setFullscreenScale(float fullscreenScale) {
+        mFullscreenScale = fullscreenScale;
+        applyScale();
     }
 
-    private void setCurveScale(float curveScale) {
-        mCurveScale = curveScale;
-        setScaleX(mCurveScale);
-        setScaleY(mCurveScale);
+    public void setGridScale(float gridScale) {
+        mGridScale = gridScale;
+        applyScale();
     }
 
-    public float getCurveScale() {
-        return mCurveScale;
+    /**
+     * Moves TaskView between carousel and 2 row grid.
+     *
+     * @param gridProgress 0 = carousel; 1 = 2 row grid.
+     */
+    public void setGridProgress(float gridProgress) {
+        mGridProgress = gridProgress;
+        applyTranslationX();
+        applyTranslationY();
+        applyScale();
     }
 
-    private void setFillDismissGapTranslationX(float x) {
-        mFillDismissGapTranslationX = x;
+    private void applyScale() {
+        float scale = 1;
+        float fullScreenProgress = EXAGGERATED_EASE.getInterpolation(mFullscreenProgress);
+        scale *= Utilities.mapRange(fullScreenProgress, 1f, mFullscreenScale);
+        float gridProgress = ACCEL_DEACCEL.getInterpolation(mGridProgress);
+        scale *= Utilities.mapRange(gridProgress, 1f, mGridScale);
+        setScaleX(scale);
+        setScaleY(scale);
+    }
+
+    private void setDismissTranslationX(float x) {
+        mDismissTranslationX = x;
         applyTranslationX();
     }
 
-    private void setFillDismissGapTranslationY(float y) {
-        mFillDismissGapTranslationY = y;
+    private void setDismissTranslationY(float y) {
+        mDismissTranslationY = y;
         applyTranslationY();
     }
 
@@ -838,17 +887,117 @@
         applyTranslationY();
     }
 
+    private void setTaskResistanceTranslationX(float x) {
+        mTaskResistanceTranslationX = x;
+        applyTranslationX();
+    }
+
+    private void setTaskResistanceTranslationY(float y) {
+        mTaskResistanceTranslationY = y;
+        applyTranslationY();
+    }
+
+    private void setFullscreenTranslationX(float fullscreenTranslationX) {
+        mFullscreenTranslationX = fullscreenTranslationX;
+        applyTranslationX();
+    }
+
+    public float getFullscreenTranslationX() {
+        return mFullscreenTranslationX;
+    }
+
+    public void setAccumulatedFullscreenTranslationX(float accumulatedFullscreenTranslationX) {
+        mAccumulatedFullscreenTranslationX = accumulatedFullscreenTranslationX;
+        applyTranslationX();
+    }
+
+    public void setGridTranslationX(float gridTranslationX) {
+        mGridTranslationX = gridTranslationX;
+        applyTranslationX();
+    }
+
+    public float getGridTranslationX() {
+        return mGridTranslationX;
+    }
+
+    public void setGridTranslationY(float gridTranslationY) {
+        mGridTranslationY = gridTranslationY;
+        applyTranslationY();
+    }
+
+    public float getGridTranslationY() {
+        return mGridTranslationY;
+    }
+
+    public void setGridOffsetTranslationX(float gridOffsetTranslationX) {
+        mGridOffsetTranslationX = gridOffsetTranslationX;
+        applyTranslationX();
+    }
+
+    public void setNonRtlVisibleOffset(float nonRtlVisibleOffset) {
+        mNonRtlVisibleOffset = nonRtlVisibleOffset;
+    }
+
+    public float getScrollAdjustment(boolean fullscreenEnabled, boolean gridEnabled) {
+        float scrollAdjustment = 0;
+        if (fullscreenEnabled) {
+            scrollAdjustment += mFullscreenTranslationX + mAccumulatedFullscreenTranslationX;
+        }
+        if (gridEnabled) {
+            scrollAdjustment += mGridTranslationX;
+        }
+        return scrollAdjustment;
+    }
+
+    public float getOffsetAdjustment(boolean fullscreenEnabled, boolean gridEnabled) {
+        float offsetAdjustment = getScrollAdjustment(fullscreenEnabled, gridEnabled);
+        if (gridEnabled) {
+            offsetAdjustment += mGridOffsetTranslationX + mNonRtlVisibleOffset;
+        }
+        return offsetAdjustment;
+    }
+
+    public float getSizeAdjustment(boolean fullscreenEnabled, boolean gridEnabled) {
+        float sizeAdjustment = 1;
+        if (fullscreenEnabled) {
+            sizeAdjustment *= mFullscreenScale;
+        }
+        if (gridEnabled) {
+            sizeAdjustment *= mGridScale;
+        }
+        return sizeAdjustment;
+    }
+
+    private void setBoxTranslationY(float boxTranslationY) {
+        mBoxTranslationY = boxTranslationY;
+        applyTranslationY();
+    }
+
     private void applyTranslationX() {
-        setTranslationX(mFillDismissGapTranslationX + mTaskOffsetTranslationX);
+        setTranslationX(mDismissTranslationX + mTaskOffsetTranslationX + mTaskResistanceTranslationX
+                + getFullscreenTrans(mFullscreenTranslationX + mAccumulatedFullscreenTranslationX)
+                + getGridTrans(mGridTranslationX + mGridOffsetTranslationX));
     }
 
     private void applyTranslationY() {
-        setTranslationY(mFillDismissGapTranslationY + mTaskOffsetTranslationY);
+        setTranslationY(
+                mDismissTranslationY + mTaskOffsetTranslationY + mTaskResistanceTranslationY
+                        + getGridTrans(mGridTranslationY) + mBoxTranslationY);
     }
 
-    public FloatProperty<TaskView> getPrimaryFillDismissGapTranslationProperty() {
+    private float getGridTrans(float endTranslation) {
+        float progress = ACCEL_DEACCEL.getInterpolation(mGridProgress);
+        return Utilities.mapRange(progress, 0, endTranslation);
+    }
+
+    public FloatProperty<TaskView> getFillDismissGapTranslationProperty() {
         return getPagedOrientationHandler().getPrimaryValue(
-                FILL_DISMISS_GAP_TRANSLATION_X, FILL_DISMISS_GAP_TRANSLATION_Y);
+                DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y);
+    }
+
+    public FloatProperty<TaskView> getDismissTaskTranslationProperty() {
+        return getPagedOrientationHandler().getSecondaryValue(
+                DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y);
     }
 
     public FloatProperty<TaskView> getPrimaryTaskOffsetTranslationProperty() {
@@ -856,6 +1005,11 @@
                 TASK_OFFSET_TRANSLATION_X, TASK_OFFSET_TRANSLATION_Y);
     }
 
+    public FloatProperty<TaskView> getTaskResistanceTranslationProperty() {
+        return getPagedOrientationHandler().getSecondaryValue(
+                TASK_RESISTANCE_TRANSLATION_X, TASK_RESISTANCE_TRANSLATION_Y);
+    }
+
     @Override
     public boolean hasOverlappingRendering() {
         // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
@@ -986,10 +1140,12 @@
     public void setFullscreenProgress(float progress) {
         progress = Utilities.boundToRange(progress, 0, 1);
         mFullscreenProgress = progress;
-        boolean isFullscreen = mFullscreenProgress > 0;
         mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE);
-        setClipChildren(!isFullscreen);
-        setClipToPadding(!isFullscreen);
+        getThumbnail().getTaskOverlay().setFullscreenProgress(progress);
+
+        applyTranslationX();
+        applyTranslationY();
+        applyScale();
 
         TaskThumbnailView thumbnail = getThumbnail();
         updateCurrentFullscreenParams(thumbnail.getPreviewPositionHelper());
@@ -1016,6 +1172,77 @@
                 previewPositionHelper);
     }
 
+    /**
+     * Updates TaskView scaling and translation required to support variable width if enabled, while
+     * ensuring TaskView fits into screen in fullscreen.
+     */
+    void updateTaskSize() {
+        ViewGroup.LayoutParams params = getLayoutParams();
+        if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+            final int thumbnailPadding = (int) getResources().getDimension(
+                    R.dimen.task_thumbnail_top_margin);
+
+            Rect lastComputedTaskSize = getRecentsView().getLastComputedTaskSize();
+            int taskWidth = lastComputedTaskSize.width();
+            int taskHeight = lastComputedTaskSize.height();
+
+            int expectedWidth;
+            int expectedHeight;
+            float thumbnailRatio = mTask != null ? mTask.getVisibleThumbnailRatio() : 0f;
+            if (isRunningTask() || thumbnailRatio == 0f) {
+                expectedWidth = taskWidth;
+                expectedHeight = taskHeight + thumbnailPadding;
+            } else {
+                int boxLength = Math.max(taskWidth, taskHeight);
+                if (thumbnailRatio > 1) {
+                    expectedWidth = boxLength;
+                    expectedHeight = (int) (boxLength / thumbnailRatio) + thumbnailPadding;
+                } else {
+                    expectedWidth = (int) (boxLength * thumbnailRatio);
+                    expectedHeight = boxLength + thumbnailPadding;
+                }
+            }
+
+            float heightDiff = (expectedHeight - thumbnailPadding - taskHeight) / 2.0f;
+            setBoxTranslationY(heightDiff);
+
+            float fullscreenScale = 1f;
+            if (expectedWidth > taskWidth) {
+                // In full screen, expectedWidth should not be larger than taskWidth.
+                fullscreenScale = taskWidth / (float) expectedWidth;
+            } else if (expectedHeight - thumbnailPadding > taskHeight) {
+                // In full screen, expectedHeight should not be larger than taskHeight.
+                fullscreenScale = taskHeight / (float) (expectedHeight - thumbnailPadding);
+            }
+            setFullscreenScale(fullscreenScale);
+
+            float widthDiff = params.width * (1 - mFullscreenScale);
+            setFullscreenTranslationX(
+                    getLayoutDirection() == LAYOUT_DIRECTION_RTL ? -widthDiff : widthDiff);
+
+            if (params.width != expectedWidth || params.height != expectedHeight) {
+                params.width = expectedWidth;
+                params.height = expectedHeight;
+                setLayoutParams(params);
+            }
+        } else {
+            setBoxTranslationY(0);
+            setFullscreenTranslationX(0);
+            setFullscreenScale(1);
+            if (params.width != ViewGroup.LayoutParams.MATCH_PARENT) {
+                params.width = ViewGroup.LayoutParams.MATCH_PARENT;
+                params.height = ViewGroup.LayoutParams.MATCH_PARENT;
+                setLayoutParams(params);
+            }
+        }
+    }
+
+
+    private float getFullscreenTrans(float endTranslation) {
+        float progress = ACCEL_DEACCEL.getInterpolation(mFullscreenProgress);
+        return Utilities.mapRange(progress, 0, endTranslation);
+    }
+
     public boolean isRunningTask() {
         if (getRecentsView() == null) {
             return false;
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index 2e7e6e0..713fd07 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -42,6 +42,7 @@
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.os.RemoteException;
+import android.util.Log;
 
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -55,6 +56,7 @@
 import com.android.launcher3.tapl.OverviewTask;
 import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.testcomponent.TestCommandReceiver;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.FailureWatcher;
 import com.android.quickstep.views.RecentsView;
@@ -172,9 +174,15 @@
 
     protected <T> T getFromRecents(Function<RecentsActivity, T> f) {
         if (!TestHelpers.isInLauncherProcess()) return null;
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.GET_RECENTS_FAILED, "getFromRecents");
+        }
         Object[] result = new Object[1];
         Wait.atMost("Failed to get from recents", () -> MAIN_EXECUTOR.submit(() -> {
             RecentsActivity activity = RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity();
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.GET_RECENTS_FAILED, "activity=" + activity);
+            }
             if (activity == null) {
                 return false;
             }
@@ -200,8 +208,13 @@
                 () -> mLauncher.getRecentTasks().size() >= 3, DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
 
         BaseOverview overview = mLauncher.getBackground().switchToOverview();
-        executeOnRecents(recents ->
-                assertTrue("Don't have at least 3 tasks", getTaskCount(recents) >= 3));
+        executeOnRecents(recents -> {
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.GET_RECENTS_FAILED, "isLoading=" +
+                        recents.<RecentsView>getOverviewPanel().isLoadingTasks());
+            }
+            assertTrue("Don't have at least 3 tasks", getTaskCount(recents) >= 3);
+        });
 
         // Test flinging forward and backward.
         overview.flingForward();
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index 0f6671d..67840d1 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -168,7 +168,7 @@
 
         Log.d(TAG, "setActiveOverlay: " + overlayPackage + "...");
         UiDevice.getInstance(getInstrumentation()).executeShellCommand(
-                "cmd overlay enable-exclusive " + overlayPackage);
+                "cmd overlay enable-exclusive --category " + overlayPackage);
 
         if (currentSysUiNavigationMode() != expectedMode) {
             final CountDownLatch latch = new CountDownLatch(1);
@@ -200,8 +200,8 @@
                 () -> launcher.getNavigationModel() == expectedMode, WAIT_TIME_MS, launcher);
 
         Wait.atMost(() -> "Switching nav mode: "
-                        + launcher.getNavigationModeMismatchError(),
-                () -> launcher.getNavigationModeMismatchError() == null,
+                        + launcher.getNavigationModeMismatchError(false),
+                () -> launcher.getNavigationModeMismatchError(false) == null,
                 WAIT_TIME_MS, launcher);
         AbstractLauncherUiTest.checkDetectedLeaks(launcher);
         return true;
diff --git a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
index 72116eb..4ca1f59 100644
--- a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
+++ b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
@@ -49,7 +49,6 @@
 import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.Until;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.tapl.Background;
@@ -58,6 +57,7 @@
 import com.android.launcher3.testcomponent.TestCommandReceiver;
 import com.android.launcher3.ui.TaplTestsLauncher3;
 import com.android.launcher3.ui.TestViewHelpers;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 
 import org.junit.Before;
diff --git a/res/drawable-hdpi/widget_resize_frame.9.png b/res/drawable-hdpi/widget_resize_frame.9.png
deleted file mode 100644
index a710932..0000000
--- a/res/drawable-hdpi/widget_resize_frame.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/widget_resize_shadow.9.png b/res/drawable-hdpi/widget_resize_shadow.9.png
deleted file mode 100644
index 7cb5214..0000000
--- a/res/drawable-hdpi/widget_resize_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/widget_resize_frame.9.png b/res/drawable-mdpi/widget_resize_frame.9.png
deleted file mode 100644
index 252482f..0000000
--- a/res/drawable-mdpi/widget_resize_frame.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/widget_resize_shadow.9.png b/res/drawable-mdpi/widget_resize_shadow.9.png
deleted file mode 100644
index a2010e2..0000000
--- a/res/drawable-mdpi/widget_resize_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/widget_resize_frame.9.png b/res/drawable-xhdpi/widget_resize_frame.9.png
deleted file mode 100644
index 563c75d..0000000
--- a/res/drawable-xhdpi/widget_resize_frame.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/widget_resize_shadow.9.png b/res/drawable-xhdpi/widget_resize_shadow.9.png
deleted file mode 100644
index 2b1ac05..0000000
--- a/res/drawable-xhdpi/widget_resize_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/widget_resize_frame.9.png b/res/drawable-xxhdpi/widget_resize_frame.9.png
deleted file mode 100644
index ea527f4..0000000
--- a/res/drawable-xxhdpi/widget_resize_frame.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/widget_resize_shadow.9.png b/res/drawable-xxhdpi/widget_resize_shadow.9.png
deleted file mode 100644
index 5412168..0000000
--- a/res/drawable-xxhdpi/widget_resize_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxxhdpi/widget_resize_frame.9.png b/res/drawable-xxxhdpi/widget_resize_frame.9.png
deleted file mode 100644
index 4644e9a..0000000
--- a/res/drawable-xxxhdpi/widget_resize_frame.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxxhdpi/widget_resize_shadow.9.png b/res/drawable-xxxhdpi/widget_resize_shadow.9.png
deleted file mode 100644
index 63cea84..0000000
--- a/res/drawable-xxxhdpi/widget_resize_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/quickstep/res/layout/search_result_thumbnail.xml b/res/drawable/bg_widgets_searchbox.xml
similarity index 71%
copy from quickstep/res/layout/search_result_thumbnail.xml
copy to res/drawable/bg_widgets_searchbox.xml
index 5062b76..81dd2aa 100644
--- a/quickstep/res/layout/search_result_thumbnail.xml
+++ b/res/drawable/bg_widgets_searchbox.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
+<!-- Copyright (C) 2021 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.
@@ -13,7 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.search.SearchResultThumbnailView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="125dp"
-    android:layout_height="125dp"/>
\ No newline at end of file
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+    <solid android:color="#FFFFF7" />
+    <corners android:radius="24dp" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/ic_expand_less.xml b/res/drawable/ic_expand_less.xml
new file mode 100644
index 0000000..8360cee
--- /dev/null
+++ b/res/drawable/ic_expand_less.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/textColorHint">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M18.59,16.41L20,15l-8,-8 -8,8 1.41,1.41L12,9.83"/>
+</vector>
diff --git a/res/drawable/ic_expand_more.xml b/res/drawable/ic_expand_more.xml
new file mode 100644
index 0000000..49e24f6
--- /dev/null
+++ b/res/drawable/ic_expand_more.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/textColorHint">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M5.41,7.59L4,9l8,8 8,-8 -1.41,-1.41L12,14.17"/>
+</vector>
diff --git a/res/drawable/ic_widget_height_decrease.xml b/res/drawable/ic_widget_height_decrease.xml
new file mode 100644
index 0000000..df704ba
--- /dev/null
+++ b/res/drawable/ic_widget_height_decrease.xml
@@ -0,0 +1,25 @@
+<!--
+Copyright (C) 2021 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/textColorPrimary">
+  <path
+      android:fillColor="#FFFFFF"
+      android:pathData="M8,19h3v3h2v-3h3l-4,-4 -4,4zM16,4h-3L13,1h-2v3L8,4l4,4 4,-4zM4,9v2h16L20,9L4,9zM4,12h16v2H4z"/>
+</vector>
diff --git a/res/drawable/ic_widget_height_increase.xml b/res/drawable/ic_widget_height_increase.xml
new file mode 100644
index 0000000..c263a4b
--- /dev/null
+++ b/res/drawable/ic_widget_height_increase.xml
@@ -0,0 +1,25 @@
+<!--
+Copyright (C) 2021 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/textColorPrimary">
+  <path
+      android:fillColor="#FFFFFF"
+      android:pathData="M4,20h16v2L4,22zM4,2h16v2L4,4zM13,9h3l-4,-4 -4,4h3v6L8,15l4,4 4,-4h-3z"/>
+</vector>
diff --git a/res/drawable/ic_widget_width_decrease.xml b/res/drawable/ic_widget_width_decrease.xml
new file mode 100644
index 0000000..2a2fad7
--- /dev/null
+++ b/res/drawable/ic_widget_width_decrease.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2021 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.
+-->
+<rotate
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  android:drawable="@drawable/ic_widget_height_decrease"
+  android:pivotX="50%"
+  android:pivotY="50%"
+  android:fromDegrees="90"
+  android:toDegrees="90" />
diff --git a/res/drawable/ic_widget_width_increase.xml b/res/drawable/ic_widget_width_increase.xml
new file mode 100644
index 0000000..89b9f40
--- /dev/null
+++ b/res/drawable/ic_widget_width_increase.xml
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2021 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.
+-->
+<rotate
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/ic_widget_height_increase"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:fromDegrees="90"
+    android:toDegrees="90" />
diff --git a/quickstep/res/layout/search_result_thumbnail.xml b/res/drawable/widget_resize_frame.xml
similarity index 60%
copy from quickstep/res/layout/search_result_thumbnail.xml
copy to res/drawable/widget_resize_frame.xml
index 5062b76..d157f5d 100644
--- a/quickstep/res/layout/search_result_thumbnail.xml
+++ b/res/drawable/widget_resize_frame.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
+<!--
+     Copyright (C) 2021 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.
@@ -13,7 +14,9 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.search.SearchResultThumbnailView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="125dp"
-    android:layout_height="125dp"/>
\ No newline at end of file
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@android:color/transparent" />
+    <corners android:radius="@android:dimen/system_app_widget_background_radius" />
+    <stroke android:width="2dp" android:color="?android:attr/colorAccent" />
+</shape>
\ No newline at end of file
diff --git a/quickstep/res/layout/search_result_thumbnail.xml b/res/drawable/widgets_tray_expand_button.xml
similarity index 64%
copy from quickstep/res/layout/search_result_thumbnail.xml
copy to res/drawable/widgets_tray_expand_button.xml
index 5062b76..8316e0f 100644
--- a/quickstep/res/layout/search_result_thumbnail.xml
+++ b/res/drawable/widgets_tray_expand_button.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
+<!-- Copyright (C) 2021 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.
@@ -13,7 +13,9 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.search.SearchResultThumbnailView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="125dp"
-    android:layout_height="125dp"/>
\ No newline at end of file
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_checked="true"
+        android:drawable="@drawable/ic_expand_less" />
+    <item android:state_checked="false"
+        android:drawable="@drawable/ic_expand_more" />
+</selector>
diff --git a/res/layout/add_item_confirmation_activity.xml b/res/layout/add_item_confirmation_activity.xml
index 830255b..b1a1efe 100644
--- a/res/layout/add_item_confirmation_activity.xml
+++ b/res/layout/add_item_confirmation_activity.xml
@@ -46,7 +46,7 @@
                 android:background="?android:attr/colorPrimaryDark"
                 android:theme="?attr/widgetsTheme">
 
-                <com.android.launcher3.dragndrop.LivePreviewWidgetCell
+                <com.android.launcher3.widget.WidgetCell
                     android:id="@+id/widget_cell"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
@@ -59,7 +59,7 @@
 
                     <include layout="@layout/widget_cell_content"  />
 
-                </com.android.launcher3.dragndrop.LivePreviewWidgetCell>
+                </com.android.launcher3.widget.WidgetCell>
             </FrameLayout>
         </LinearLayout>
     </ScrollView>
diff --git a/res/layout/all_apps.xml b/res/layout/all_apps.xml
index 0041c9a..8ed16c7 100644
--- a/res/layout/all_apps.xml
+++ b/res/layout/all_apps.xml
@@ -40,35 +40,7 @@
 
         <include layout="@layout/floating_header_content" />
 
-        <com.android.launcher3.allapps.PersonalWorkSlidingTabStrip
-            android:id="@+id/tabs"
-            android:layout_width="match_parent"
-            android:layout_height="@dimen/all_apps_header_tab_height"
-            android:layout_marginLeft="@dimen/all_apps_tabs_side_padding"
-            android:layout_marginRight="@dimen/all_apps_tabs_side_padding"
-            android:orientation="horizontal"
-            style="@style/TextHeadline">
-
-            <Button
-                android:id="@+id/tab_personal"
-                android:layout_width="0dp"
-                android:layout_height="match_parent"
-                android:layout_weight="1"
-                android:background="?android:attr/selectableItemBackground"
-                android:text="@string/all_apps_personal_tab"
-                android:textColor="@color/all_apps_tab_text"
-                android:textSize="14sp" />
-
-            <Button
-                android:id="@+id/tab_work"
-                android:layout_width="0dp"
-                android:layout_height="match_parent"
-                android:layout_weight="1"
-                android:background="?android:attr/selectableItemBackground"
-                android:text="@string/all_apps_work_tab"
-                android:textColor="@color/all_apps_tab_text"
-                android:textSize="14sp" />
-        </com.android.launcher3.allapps.PersonalWorkSlidingTabStrip>
+        <include layout="@layout/personal_work_tabs" />
     </com.android.launcher3.allapps.FloatingHeaderView>
 
     <include
diff --git a/res/layout/app_widget_resize_frame.xml b/res/layout/app_widget_resize_frame.xml
index 12561b6..dfce946 100644
--- a/res/layout/app_widget_resize_frame.xml
+++ b/res/layout/app_widget_resize_frame.xml
@@ -18,15 +18,20 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:background="@drawable/widget_resize_shadow"
-    android:foreground="@drawable/widget_resize_frame"
-    android:foregroundTint="?attr/workspaceTextColor"
     android:padding="0dp">
 
     <FrameLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent" >
 
+        <!-- Frame -->
+        <ImageView
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_gravity="center"
+            android:layout_margin="@dimen/resize_frame_margin"
+            android:src="@drawable/widget_resize_frame" />
+
         <!-- Left -->
         <ImageView
             android:layout_width="wrap_content"
@@ -34,7 +39,7 @@
             android:layout_gravity="left|center_vertical"
             android:layout_marginLeft="@dimen/widget_handle_margin"
             android:src="@drawable/ic_widget_resize_handle"
-            android:tint="?attr/workspaceTextColor" />
+            android:tint="?android:attr/colorAccent" />
 
         <!-- Top -->
         <ImageView
@@ -43,7 +48,7 @@
             android:layout_gravity="top|center_horizontal"
             android:layout_marginTop="@dimen/widget_handle_margin"
             android:src="@drawable/ic_widget_resize_handle"
-            android:tint="?attr/workspaceTextColor" />
+            android:tint="?android:attr/colorAccent" />
 
         <!-- Right -->
         <ImageView
@@ -52,7 +57,7 @@
             android:layout_gravity="right|center_vertical"
             android:layout_marginRight="@dimen/widget_handle_margin"
             android:src="@drawable/ic_widget_resize_handle"
-            android:tint="?attr/workspaceTextColor" />
+            android:tint="?android:attr/colorAccent" />
 
         <!-- Bottom -->
         <ImageView
@@ -61,7 +66,7 @@
             android:layout_gravity="bottom|center_horizontal"
             android:layout_marginBottom="@dimen/widget_handle_margin"
             android:src="@drawable/ic_widget_resize_handle"
-            android:tint="?attr/workspaceTextColor" />
+            android:tint="?android:attr/colorAccent" />
 
     </FrameLayout>
 </com.android.launcher3.AppWidgetResizeFrame>
\ No newline at end of file
diff --git a/res/layout/folder_application.xml b/res/layout/folder_application.xml
index 32a5419..1cdee08 100644
--- a/res/layout/folder_application.xml
+++ b/res/layout/folder_application.xml
@@ -21,4 +21,5 @@
     android:textColor="?attr/folderTextColor"
     android:includeFontPadding="false"
     android:hapticFeedbackEnabled="false"
-    launcher:iconDisplay="folder" />
+    launcher:iconDisplay="folder"
+    launcher:centerVertically="true" />
diff --git a/res/layout/keyboard_drag_and_drop.xml b/res/layout/keyboard_drag_and_drop.xml
new file mode 100644
index 0000000..e9463c4
--- /dev/null
+++ b/res/layout/keyboard_drag_and_drop.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.keyboard.KeyboardDragAndDropView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:focusable="true"
+    android:orientation="vertical"
+    android:elevation="6dp">
+
+    <TextView
+        android:id="@+id/label"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:background="?attr/folderFillColor"
+        android:padding="8dp"
+        android:textColor="?attr/folderTextColor"
+        />
+
+</com.android.launcher3.keyboard.KeyboardDragAndDropView>
\ No newline at end of file
diff --git a/res/layout/personal_work_tabs.xml b/res/layout/personal_work_tabs.xml
new file mode 100644
index 0000000..5fb5bcb
--- /dev/null
+++ b/res/layout/personal_work_tabs.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.workprofile.PersonalWorkSlidingTabStrip
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/tabs"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/all_apps_header_tab_height"
+    android:layout_marginLeft="@dimen/all_apps_tabs_side_padding"
+    android:layout_marginRight="@dimen/all_apps_tabs_side_padding"
+    android:orientation="horizontal"
+    android:elevation="2dp"
+    style="@style/TextHeadline">
+
+    <Button
+        android:id="@+id/tab_personal"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:background="?android:attr/selectableItemBackground"
+        android:text="@string/all_apps_personal_tab"
+        android:textColor="@color/all_apps_tab_text"
+        android:textSize="14sp" />
+
+    <Button
+        android:id="@+id/tab_work"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:background="?android:attr/selectableItemBackground"
+        android:text="@string/all_apps_work_tab"
+        android:textColor="@color/all_apps_tab_text"
+        android:textSize="14sp" />
+</com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip>
\ No newline at end of file
diff --git a/res/layout/secondary_launcher.xml b/res/layout/secondary_launcher.xml
index fdf4446..e3c60ec 100644
--- a/res/layout/secondary_launcher.xml
+++ b/res/layout/secondary_launcher.xml
@@ -67,7 +67,7 @@
             android:paddingTop="@dimen/all_apps_header_top_padding"
             android:orientation="vertical" >
 
-            <com.android.launcher3.allapps.PersonalWorkSlidingTabStrip
+            <com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip
                 android:id="@+id/tabs"
                 android:layout_width="match_parent"
                 android:layout_height="@dimen/all_apps_header_tab_height"
@@ -97,7 +97,7 @@
                     android:textAllCaps="true"
                     android:textColor="@color/all_apps_tab_text"
                     android:textSize="14sp" />
-            </com.android.launcher3.allapps.PersonalWorkSlidingTabStrip>
+            </com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip>
         </com.android.launcher3.allapps.FloatingHeaderView>
 
         <com.android.launcher3.allapps.search.AppsSearchContainerLayout
diff --git a/res/layout/user_folder_icon_normalized.xml b/res/layout/user_folder_icon_normalized.xml
index 923352e..d38a77a 100644
--- a/res/layout/user_folder_icon_normalized.xml
+++ b/res/layout/user_folder_icon_normalized.xml
@@ -27,15 +27,12 @@
         android:clipToPadding="false"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:paddingLeft="8dp"
-        android:paddingRight="8dp"
-        android:paddingTop="16dp"
         launcher:pageIndicator="@+id/folder_page_indicator" />
 
     <LinearLayout
         android:id="@+id/folder_footer"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="48dp"
         android:clipChildren="false"
         android:orientation="horizontal"
         android:paddingLeft="12dp"
@@ -53,13 +50,10 @@
             android:gravity="center_horizontal"
             android:hint="@string/folder_hint_text"
             android:imeOptions="flagNoExtractUi"
-            android:paddingBottom="@dimen/folder_label_padding_bottom"
-            android:paddingTop="@dimen/folder_label_padding_top"
             android:singleLine="true"
             android:textColor="?attr/folderTextColor"
             android:textColorHighlight="?android:attr/colorControlHighlight"
-            android:textColorHint="?attr/folderHintColor"
-            android:textSize="@dimen/folder_label_text_size" />
+            android:textColorHint="?attr/folderHintColor"/>
 
         <com.android.launcher3.pageindicators.PageIndicatorDots
             android:id="@+id/folder_page_indicator"
diff --git a/res/layout/widget_cell.xml b/res/layout/widget_cell.xml
index 148a99b..55dd1de 100644
--- a/res/layout/widget_cell.xml
+++ b/res/layout/widget_cell.xml
@@ -15,12 +15,13 @@
 -->
 <com.android.launcher3.widget.WidgetCell
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content"
+    android:layout_width="0dp"
     android:layout_height="wrap_content"
+    android:paddingHorizontal="@dimen/widget_cell_horizontal_padding"
+    android:paddingVertical="@dimen/widget_cell_vertical_padding"
     android:layout_weight="1"
     android:orientation="vertical"
     android:focusable="true"
-    android:background="?android:attr/colorPrimaryDark"
     android:gravity="center_horizontal">
 
     <include layout="@layout/widget_cell_content"  />
diff --git a/res/layout/widget_cell_content.xml b/res/layout/widget_cell_content.xml
index 64f2362..65a49ab 100644
--- a/res/layout/widget_cell_content.xml
+++ b/res/layout/widget_cell_content.xml
@@ -17,47 +17,47 @@
     android:layout_width="wrap_content"
     android:layout_height="wrap_content">
 
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:paddingTop="@dimen/widget_preview_label_vertical_padding"
-        android:paddingBottom="@dimen/widget_preview_label_vertical_padding"
-        android:paddingLeft="@dimen/widget_preview_label_horizontal_padding"
-        android:paddingRight="@dimen/widget_preview_label_horizontal_padding"
-        android:orientation="horizontal">
-
-        <!-- The name of the widget. -->
-        <TextView
-            android:id="@+id/widget_name"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_weight="1"
-            android:ellipsize="end"
-            android:fadingEdge="horizontal"
-            android:gravity="start"
-            android:singleLine="true"
-            android:maxLines="1"
-            android:textColor="?android:attr/textColorSecondary"
-            android:textSize="14sp" />
-
-        <!-- The original dimensions of the widget (can't be the same text as above due to different
-             style. -->
-        <TextView
-            android:id="@+id/widget_dims"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginStart="5dp"
-            android:layout_marginLeft="5dp"
-            android:textColor="?android:attr/textColorSecondary"
-            android:textSize="14sp"
-            android:alpha="0.8" />
-    </LinearLayout>
-
     <!-- The image of the widget. This view does not support padding. Any placement adjustment
          should be done using margins. -->
     <com.android.launcher3.widget.WidgetImageView
         android:id="@+id/widget_preview"
-        android:layout_width="match_parent"
+        android:layout_width="wrap_content"
         android:layout_height="0dp"
-        android:layout_weight="1" />
+        android:layout_weight="1"
+        android:layout_marginVertical="8dp" />
+
+    <!-- The name of the widget. -->
+    <TextView
+        android:id="@+id/widget_name"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:ellipsize="end"
+        android:fadingEdge="horizontal"
+        android:gravity="center_horizontal"
+        android:singleLine="true"
+        android:maxLines="1"
+        android:textColor="?android:attr/textColorSecondary"
+        android:textSize="14sp" />
+
+    <!-- The original dimensions of the widget (can't be the same text as above due to different
+         style. -->
+    <TextView
+        android:id="@+id/widget_dims"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:textColor="?android:attr/textColorSecondary"
+        android:textSize="14sp"
+        android:alpha="0.8" />
+
+    <TextView
+        android:id="@+id/widget_description"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:textSize="12sp"
+        android:maxLines="2"
+        android:ellipsize="end"
+        android:fadingEdge="horizontal" />
+
 </merge>
\ No newline at end of file
diff --git a/res/layout/widgets_bottom_sheet.xml b/res/layout/widgets_bottom_sheet.xml
index 3fdfc96..c1b2cbf 100644
--- a/res/layout/widgets_bottom_sheet.xml
+++ b/res/layout/widgets_bottom_sheet.xml
@@ -44,11 +44,16 @@
         android:textSize="14sp"
         android:text="@string/long_press_widget_to_add"/>
 
-    <include layout="@layout/widgets_scroll_container"
-         android:id="@+id/widgets"
-         android:layout_width="wrap_content"
-         android:layout_height="wrap_content"
-         android:layout_marginTop="45dp"
-         android:layout_marginBottom="40dp"/>
+    <ScrollView
+        android:id="@+id/widgets_table_scroll_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="45dp"
+        android:layout_marginBottom="40dp">
+        <include layout="@layout/widgets_table_container"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal" />
+    </ScrollView>
 
 </com.android.launcher3.widget.WidgetsBottomSheet>
\ No newline at end of file
diff --git a/res/layout/widgets_full_sheet.xml b/res/layout/widgets_full_sheet.xml
index f507a88..28a8c6f 100644
--- a/res/layout/widgets_full_sheet.xml
+++ b/res/layout/widgets_full_sheet.xml
@@ -13,7 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.widget.WidgetsFullSheet
+<com.android.launcher3.widget.picker.WidgetsFullSheet
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
@@ -27,12 +27,6 @@
         android:background="?android:attr/colorPrimary"
         android:elevation="4dp">
 
-        <com.android.launcher3.widget.WidgetsRecyclerView
-            android:id="@+id/widgets_list_view"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:clipToPadding="false" />
-
         <!-- Fast scroller popup -->
         <TextView
             android:id="@+id/fast_scroller_popup"
@@ -49,4 +43,4 @@
             android:layout_alignParentTop="true"
             android:layout_marginEnd="@dimen/fastscroll_end_margin" />
     </com.android.launcher3.views.TopRoundedCornerView>
-</com.android.launcher3.widget.WidgetsFullSheet>
\ No newline at end of file
+</com.android.launcher3.widget.picker.WidgetsFullSheet>
\ No newline at end of file
diff --git a/res/layout/widgets_full_sheet_paged_view.xml b/res/layout/widgets_full_sheet_paged_view.xml
new file mode 100644
index 0000000..8125db8
--- /dev/null
+++ b/res/layout/widgets_full_sheet_paged_view.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto">
+
+    <include layout="@layout/personal_work_tabs"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="16dp" />
+
+    <com.android.launcher3.workprofile.PersonalWorkPagedView
+        android:id="@+id/widgets_view_pager"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:clipToPadding="false"
+        android:descendantFocusability="afterDescendants"
+        launcher:pageIndicator="@+id/tabs">
+
+        <com.android.launcher3.widget.picker.WidgetsRecyclerView
+            android:id="@+id/primary_widgets_list_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:clipToPadding="false" />
+
+        <com.android.launcher3.widget.picker.WidgetsRecyclerView
+            android:id="@+id/work_widgets_list_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:clipToPadding="false" />
+
+    </com.android.launcher3.workprofile.PersonalWorkPagedView>
+
+</merge>
\ No newline at end of file
diff --git a/quickstep/res/layout/search_result_thumbnail.xml b/res/layout/widgets_full_sheet_recyclerview.xml
similarity index 70%
copy from quickstep/res/layout/search_result_thumbnail.xml
copy to res/layout/widgets_full_sheet_recyclerview.xml
index 5062b76..fbe559c 100644
--- a/quickstep/res/layout/search_result_thumbnail.xml
+++ b/res/layout/widgets_full_sheet_recyclerview.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
+<!-- Copyright (C) 2021 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.
@@ -13,7 +13,9 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.search.SearchResultThumbnailView
+<com.android.launcher3.widget.picker.WidgetsRecyclerView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="125dp"
-    android:layout_height="125dp"/>
\ No newline at end of file
+    android:id="@+id/primary_widgets_list_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:clipToPadding="false" />
\ No newline at end of file
diff --git a/res/layout/widgets_full_sheet_search_and_recommendations.xml b/res/layout/widgets_full_sheet_search_and_recommendations.xml
new file mode 100644
index 0000000..9a6f922
--- /dev/null
+++ b/res/layout/widgets_full_sheet_search_and_recommendations.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/search_and_recommendations_container"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="16dp"
+    android:orientation="vertical">
+    <View
+        android:id="@+id/collapse_handle"
+        android:layout_width="48dp"
+        android:layout_height="2dp"
+        android:layout_gravity="center_horizontal"
+        android:background="@color/popup_color_primary_dark"/>
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:textSize="24sp"
+        android:layout_marginTop="16dp"
+        android:text="@string/widget_button_text"/>
+    <!-- Disable the search bar because it has not been implemented. -->
+    <EditText
+        android:id="@+id/widgets_search_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:layout_marginTop="16dp"
+        android:background="@drawable/bg_widgets_searchbox"
+        android:drawablePadding="8dp"
+        android:drawableStart="@drawable/ic_allapps_search"
+        android:hint="@string/widgets_full_sheet_search_bar_hint"
+        android:padding="12dp" />
+</LinearLayout>
diff --git a/res/layout/widgets_list_row_header.xml b/res/layout/widgets_list_row_header.xml
new file mode 100644
index 0000000..041e007
--- /dev/null
+++ b/res/layout/widgets_list_row_header.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.widget.picker.WidgetsListHeader xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/widgets_list_header"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?android:attr/selectableItemBackground"
+    android:paddingVertical="20dp"
+    android:orientation="horizontal">
+
+    <ImageView
+        android:id="@+id/app_icon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="16dp"
+        android:importantForAccessibility="no"
+        tools:src="@drawable/ic_corp"/>
+
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:layout_weight="1"
+        android:orientation="vertical"
+        android:focusable="true"
+        android:descendantFocusability="afterDescendants">
+
+        <TextView
+            android:id="@+id/app_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:textColor="?android:attr/textColorPrimary"
+            android:textSize="16sp"
+            tools:text="App name" />
+
+        <TextView
+            android:id="@+id/app_subtitle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            tools:text="m widgets, n shortcuts" />
+
+    </LinearLayout>
+
+    <!-- This checkbox is not clickable. The outermost LinearLayout is responsible to handle all
+         click event and update the checkbox state. -->
+    <CheckBox
+        android:id="@+id/toggle"
+        android:layout_marginHorizontal="16dp"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:layout_alignParentEnd="true"
+        android:clickable="false"
+        android:button="@drawable/widgets_tray_expand_button"/>
+
+</com.android.launcher3.widget.picker.WidgetsListHeader>
\ No newline at end of file
diff --git a/res/layout/widgets_list_row_view.xml b/res/layout/widgets_list_row_view.xml
index eec57a5..5942ba6 100644
--- a/res/layout/widgets_list_row_view.xml
+++ b/res/layout/widgets_list_row_view.xml
@@ -45,5 +45,5 @@
         launcher:iconSizeOverride="@dimen/widget_section_icon_size"
         launcher:layoutHorizontal="true" />
 
-    <include layout="@layout/widgets_scroll_container" />
+    <include layout="@layout/widgets_table_container" />
 </LinearLayout>
diff --git a/res/layout/widgets_scroll_container.xml b/res/layout/widgets_scroll_container.xml
deleted file mode 100644
index fc509d1..0000000
--- a/res/layout/widgets_scroll_container.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<HorizontalScrollView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/widgets_scroll_container"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:background="?android:attr/colorPrimaryDark"
-    android:scrollbars="none">
-    <LinearLayout
-        android:id="@+id/widgets_cell_list"
-        style="@style/TextTitle"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:paddingStart="0dp"
-        android:paddingEnd="0dp"
-        android:orientation="horizontal"
-        android:showDividers="none"/>
-</HorizontalScrollView>
\ No newline at end of file
diff --git a/quickstep/res/layout/search_result_thumbnail.xml b/res/layout/widgets_table_container.xml
similarity index 69%
copy from quickstep/res/layout/search_result_thumbnail.xml
copy to res/layout/widgets_table_container.xml
index 5062b76..c4dfe7e 100644
--- a/quickstep/res/layout/search_result_thumbnail.xml
+++ b/res/layout/widgets_table_container.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2020 The Android Open Source Project
+<!-- Copyright (C) 2021 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.
@@ -13,7 +13,10 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.search.SearchResultThumbnailView
+<TableLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="125dp"
-    android:layout_height="125dp"/>
\ No newline at end of file
+    android:id="@+id/widgets_table"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginHorizontal="8dp"
+    android:background="?android:attr/colorPrimaryDark" />
diff --git a/res/values-sw720dp/config.xml b/res/values-sw720dp/config.xml
index 1f401c4..ec07591 100644
--- a/res/values-sw720dp/config.xml
+++ b/res/values-sw720dp/config.xml
@@ -1,5 +1,4 @@
 <resources>
-    <bool name="config_largeHeap">true</bool>
 
 <!-- All Apps & Widgets -->
     <!-- Out of 100, the percent to shrink the workspace during spring loaded mode. -->
diff --git a/res/values-sw720dp/styles.xml b/res/values-sw720dp/styles.xml
index f322e9f..c1e6eca 100644
--- a/res/values-sw720dp/styles.xml
+++ b/res/values-sw720dp/styles.xml
@@ -18,16 +18,6 @@
 -->
 
 <resources>
-
-    <style name="BaseLauncherTheme" parent="@android:style/Theme.DeviceDefault.Light.NoActionBar">
-        <item name="android:windowBackground">@android:color/transparent</item>
-        <item name="android:colorBackgroundCacheHint">@null</item>
-        <item name="android:windowShowWallpaper">true</item>
-        <item name="android:windowNoTitle">true</item>
-        <item name="android:windowActionModeOverlay">true</item>
-        <item name="android:colorEdgeEffect">?android:attr/textColorSecondary</item>
-    </style>
-
     <!-- Workspace -->
     <style name="DropTargetButton" parent="DropTargetButtonBase">
         <item name="android:paddingLeft">60dp</item>
@@ -36,5 +26,4 @@
         <item name="android:shadowDy">0.0</item>
         <item name="android:shadowRadius">2.0</item>
     </style>
-
 </resources>
\ No newline at end of file
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
new file mode 100644
index 0000000..6baf39e
--- /dev/null
+++ b/res/values-v31/colors.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2021, 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="popup_color_primary_light">@android:color/system_primary_50</color>
+    <color name="popup_color_secondary_light">@android:color/system_primary_100</color>
+    <color name="popup_color_tertiary_light">@android:color/system_primary_300</color>
+    <color name="popup_color_primary_dark">@android:color/system_primary_800</color>
+    <color name="popup_color_secondary_dark">@android:color/system_primary_900</color>
+    <color name="popup_color_tertiary_dark">@android:color/system_primary_700</color>
+
+    <color name="workspace_text_color_light">@android:color/system_primary_50</color>
+    <color name="workspace_text_color_dark">@android:color/system_primary_900</color>
+
+    <color name="text_color_primary_dark">@android:color/system_primary_50</color>
+    <color name="text_color_secondary_dark">@android:color/system_primary_200</color>
+    <color name="text_color_tertiary_dark">@android:color/system_primary_400</color>
+</resources>
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 96c30b5..4078ef4 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -57,6 +57,7 @@
             <enum name="widget_section" value="3" />
             <enum name="shortcut_popup" value="4" />
             <enum name="hero_app" value="5" />
+            <enum name="taskbar" value="6" />
         </attr>
         <attr name="centerVertically" format="boolean" />
     </declare-styleable>
@@ -132,6 +133,18 @@
         <attr name="numAllAppsColumns" format="integer" />
         <attr name="defaultLayoutId" format="reference" />
         <attr name="demoModeLayoutId" format="reference" />
+        <attr name="isScalable" format="boolean" />
+
+    </declare-styleable>
+
+    <declare-styleable name="DevicePadding">
+        <attr name="maxEmptySpace" format="dimension" />
+    </declare-styleable>
+
+    <declare-styleable name="DevicePaddingFormula">
+        <attr name="a" format="float|dimension" />
+        <attr name="b" format="float|dimension" />
+        <attr name="c" format="float|dimension" />
     </declare-styleable>
 
     <declare-styleable name="ProfileDisplayOption">
@@ -139,6 +152,12 @@
         <attr name="minWidthDps" format="float" />
         <attr name="minHeightDps" format="float" />
 
+        <!-- These min cell values are only used if GridDisplayOption#isScalable is true-->
+        <attr name="minCellHeightDps" format="float" />
+        <attr name="minCellWidthDps" format="float" />
+
+        <attr name="borderSpacingDps" format="float" />
+
         <attr name="iconImageSize" format="float" />
         <!-- landscapeIconSize defaults to iconSize, if not specified -->
         <attr name="landscapeIconSize" format="float" />
@@ -184,4 +203,8 @@
         <attr name="android:name" />
         <attr name="android:id" />
     </declare-styleable>
+
+    <declare-styleable name="WidgetsListRowHeader">
+        <attr name="appIconSize" format="dimension" />
+    </declare-styleable>
 </resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 78c2df6..0b30253 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -40,4 +40,19 @@
     <color name="gesture_tutorial_fake_previous_task_view_color">#9CCC65</color> <!-- Light Green -->
     <color name="gesture_tutorial_action_button_label_color">#FFFFFFFF</color>
     <color name="gesture_tutorial_primary_color">#1A73E8</color> <!-- Blue -->
+
+    <color name="popup_color_primary_light">#FFF</color>
+    <color name="popup_color_secondary_light">#F1F3F4</color>
+    <color name="popup_color_tertiary_light">#E0E0E0</color> <!-- Gray 300 -->
+    <color name="popup_color_primary_dark">#3C4043</color> <!-- Gray 800 -->
+    <color name="popup_color_secondary_dark">#202124</color>
+    <color name="popup_color_tertiary_dark">#757575</color> <!-- Gray 600 -->
+
+    <color name="workspace_text_color_light">#FFF</color>
+    <color name="workspace_text_color_dark">#FF212121</color>
+
+    <color name="text_color_primary_dark">#FFFFFFFF</color>
+    <color name="text_color_secondary_dark">#FFFFFFFF</color>
+    <color name="text_color_tertiary_dark">#CCFFFFFF</color>
+
 </resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index 41d1a12..89415b8 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -24,7 +24,7 @@
 
     <!-- AllApps & Launcher transitions -->
     <!-- Out of 100, the percent to shrink the workspace during spring loaded mode. -->
-    <integer name="config_workspaceSpringLoadShrinkPercentage">90</integer>
+    <integer name="config_workspaceSpringLoadShrinkPercentage">85</integer>
 
     <!-- The duration of the animation from search hint to text entry -->
     <integer name="config_searchHintAnimationDuration">50</integer>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index cf51f77..da43758 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -20,10 +20,12 @@
 
     <!-- Dynamic Grid -->
     <dimen name="dynamic_grid_edge_margin">8dp</dimen>
-    <dimen name="dynamic_grid_icon_drawable_padding">8dp</dimen>
+    <dimen name="dynamic_grid_left_right_margin">8dp</dimen>
+    <dimen name="dynamic_grid_icon_drawable_padding">7dp</dimen>
     <!-- Minimum space between workspace and hotseat in spring loaded mode -->
     <dimen name="dynamic_grid_min_spring_loaded_space">8dp</dimen>
 
+    <dimen name="dynamic_grid_cell_border_spacing">16dp</dimen>
     <dimen name="dynamic_grid_cell_layout_padding">5.5dp</dimen>
     <dimen name="dynamic_grid_cell_padding_x">8dp</dimen>
 
@@ -35,6 +37,9 @@
     <dimen name="dynamic_grid_hotseat_extra_vertical_size">34dp</dimen>
     <dimen name="dynamic_grid_hotseat_side_padding">0dp</dimen>
 
+    <!-- Scalable Grid -->
+    <dimen name="scalable_grid_left_right_margin">22dp</dimen>
+
     <!-- Workspace page indicator -->
     <dimen name="workspace_page_indicator_height">24dp</dimen>
     <dimen name="workspace_page_indicator_line_height">1dp</dimen>
@@ -47,6 +52,7 @@
 <!-- App Widget resize frame -->
     <dimen name="widget_handle_margin">13dp</dimen>
     <dimen name="resize_frame_background_padding">24dp</dimen>
+    <dimen name="resize_frame_margin">22dp</dimen>
 
 <!-- Fast scroll -->
     <dimen name="fastscroll_track_min_width">6dp</dimen>
@@ -88,7 +94,6 @@
     <!-- The size of corner radius of the arrow in the arrow toast. -->
     <dimen name="arrow_toast_corner_radius">2dp</dimen>
 
-
 <!-- Search bar in All Apps -->
     <dimen name="all_apps_header_max_elevation">3dp</dimen>
     <dimen name="all_apps_header_scroll_to_elevation">16dp</dimen>
@@ -100,8 +105,8 @@
     <dimen name="work_profile_footer_text_size">16sp</dimen>
 
 <!-- Widget tray -->
-    <dimen name="widget_preview_label_vertical_padding">8dp</dimen>
-    <dimen name="widget_preview_label_horizontal_padding">16dp</dimen>
+    <dimen name="widget_cell_vertical_padding">8dp</dimen>
+    <dimen name="widget_cell_horizontal_padding">16dp</dimen>
 
     <dimen name="widget_preview_shadow_blur">0.5dp</dimen>
     <dimen name="widget_preview_key_shadow_distance">1dp</dimen>
@@ -139,16 +144,19 @@
     <dimen name="drag_flingToDeleteMinVelocity">-1500dp</dimen>
 
     <dimen name="spring_loaded_panel_border">1dp</dimen>
+    <dimen name="keyboard_drag_stroke_width">4dp</dimen>
 
 <!-- Folders -->
     <dimen name="page_indicator_dot_size">8dp</dimen>
 
     <dimen name="folder_cell_x_padding">9dp</dimen>
     <dimen name="folder_cell_y_padding">6dp</dimen>
-    <dimen name="folder_child_text_size">13sp</dimen>
-    <dimen name="folder_label_padding_top">4dp</dimen>
-    <dimen name="folder_label_padding_bottom">12dp</dimen>
-    <dimen name="folder_label_text_size">14sp</dimen>
+    <!-- label text size = workspace text size multiplied by this scale -->
+    <dimen name="folder_label_text_scale">1.14</dimen>
+    <dimen name="folder_label_height">48dp</dimen>
+
+    <dimen name="folder_content_padding_left_right">8dp</dimen>
+    <dimen name="folder_content_padding_top">16dp</dimen>
 
 <!-- Sizes for managed profile badges -->
     <dimen name="profile_badge_size">24dp</dimen>
@@ -167,7 +175,8 @@
 
 <!-- Deep shortcuts -->
     <dimen name="deep_shortcuts_elevation">9dp</dimen>
-    <dimen name="bg_popup_item_width">260dp</dimen>
+    <!-- also update deep_shortcuts_divider_width -->
+    <dimen name="bg_popup_item_width">234dp</dimen>
     <dimen name="bg_popup_item_height">56dp</dimen>
     <dimen name="bg_popup_item_condensed_height">48dp</dimen>
     <dimen name="pre_drag_view_scale">6dp</dimen>
@@ -179,18 +188,16 @@
     <dimen name="popup_padding_start">10dp</dimen>
     <dimen name="popup_padding_end">16dp</dimen>
     <dimen name="popup_vertical_padding">4dp</dimen>
-    <dimen name="popup_arrow_width">10dp</dimen>
-    <dimen name="popup_arrow_height">8dp</dimen>
-    <dimen name="popup_arrow_vertical_offset">-2dp</dimen>
+    <dimen name="popup_arrow_width">12dp</dimen>
+    <dimen name="popup_arrow_height">10dp</dimen>
+    <dimen name="popup_arrow_vertical_offset">-1dp</dimen>
     <!-- popup_padding_start + deep_shortcut_icon_size / 2 -->
-    <dimen name="popup_arrow_horizontal_center_start">28dp</dimen>
-    <!-- popup_padding_end + deep_shortcut_drag_handle_size / 2 -->
-    <dimen name="popup_arrow_horizontal_center_end">24dp</dimen>
+    <dimen name="popup_arrow_horizontal_center_offset">28dp</dimen>
     <dimen name="popup_arrow_corner_radius">2dp</dimen>
     <!-- popup_padding_start + icon_size + 10dp -->
     <dimen name="deep_shortcuts_text_padding_start">56dp</dimen>
     <!-- popup_item_width - deep_shortcuts_text_padding_start -->
-    <dimen name="deep_shortcuts_divider_width">164dp</dimen>
+    <dimen name="deep_shortcuts_divider_width">178dp</dimen>
     <dimen name="system_shortcut_icon_size">24dp</dimen>
     <!-- popup_arrow_center_start - system_shortcut_icon_size / 2 -->
     <dimen name="system_shortcut_margin_start">16dp</dimen>
@@ -250,10 +257,15 @@
 
     <!-- Search related -->
     <dimen name="search_hero_title_size">16sp</dimen>
-    <dimen name="search_hero_subtitle_size">15sp</dimen>
+    <dimen name="search_hero_subtitle_size">14sp</dimen>
     <dimen name="search_hero_inline_button_size">12sp</dimen>
     <dimen name="search_settings_icon_size">36dp</dimen>
     <dimen name="search_settings_icon_vertical_offset">16dp</dimen>
     <dimen name="search_line_spacing">4dp</dimen>
+    <dimen name="search_decoration_corner_radius">28dp</dimen>
+    <dimen name="search_decoration_padding">1dp</dimen>
+
+<!-- Taskbar related (placeholders to compile in Launcher3 without Quickstep) -->
+    <dimen name="taskbar_size">0dp</dimen>
 
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3425f91..44b5ee7 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -37,8 +37,6 @@
     <string name="shortcut_not_available">Shortcut isn\'t available</string>
     <!-- User visible name for the launcher/home screen. [CHAR_LIMIT=30] -->
     <string name="home_screen">Home</string>
-    <!-- Label for showing custom action list of a shortcut or widget. [CHAR_LIMIT=30] -->
-    <string name="custom_actions">Custom actions</string>
 
     <!-- Widgets -->
     <!-- Message to tell the user to press and hold on a widget to add it [CHAR_LIMIT=50] -->
@@ -54,6 +52,28 @@
     <string name="add_item_request_drag_hint">Touch &amp; hold to place manually</string>
     <!-- Button label to automatically add icon on home screen [CHAR_LIMIT=50] -->
     <string name="place_automatically">Add automatically</string>
+    <!-- Label for showing the number of widgets an app has in the full widgets picker.
+         [CHAR_LIMIT=25] -->
+    <plurals name="widgets_count">
+        <item quantity="one"><xliff:g id="widgets_count" example="1">%1$d</xliff:g> widget</item>
+        <item quantity="other"><xliff:g id="widgets_count" example="2">%1$d</xliff:g> widgets</item>
+    </plurals>
+    <!-- Label for showing the number of shortcut an app has in the full widgets picker.
+         [CHAR_LIMIT=25] -->
+    <plurals name="shortcuts_count">
+        <item quantity="one"><xliff:g id="shortcuts_count" example="1">%1$d</xliff:g> shortcut</item>
+        <item quantity="other"><xliff:g id="shortcuts_count" example="2">%1$d</xliff:g> shortcuts</item>
+    </plurals>
+    <!-- Label for showing both the number of widgets and shortcuts an app has in the full widgets
+         picker. [CHAR_LIMIT=50] -->
+    <string name="widgets_and_shortcuts_count"><xliff:g id="widgets_count" example="5 widgets">%1$s</xliff:g>, <xliff:g id="shortcuts_count" example="1 shortcut">%2$s</xliff:g></string>
+
+    <!-- Text for both the tile of a popup view, which shows all available widgets installed on
+         the device, and the text of a button, which opens this popup view. [CHAR LIMIT=30]-->
+    <string name="widget_button_text">Widgets</string>
+    <!-- Search bar text shown in the popup view showing all available widgets installed on the
+         device. [CHAR_LIMIT=50] -->
+    <string name="widgets_full_sheet_search_bar_hint">Search</string>
 
     <!-- All Apps -->
     <!-- Search bar text in the apps view. [CHAR_LIMIT=50] -->
@@ -182,8 +202,6 @@
     <string name="folder_name_format_overflow">Folder: <xliff:g id="name" example="Games">%1$s</xliff:g>, <xliff:g id="size" example="2">%2$d</xliff:g> or more items</string>
 
     <!-- Strings for the customization mode -->
-    <!-- Text for widget add button [CHAR LIMIT=30]-->
-    <string name="widget_button_text">Widgets</string>
     <!-- Text for wallpaper change button [CHAR LIMIT=30]-->
     <string name="wallpaper_button_text">Wallpapers</string>
     <!-- Text for wallpaper change button [CHAR LIMIT=30]-->
@@ -232,6 +250,8 @@
     <string name="abandoned_promise_explanation">The app for this icon isn\'t installed.
         You can remove it, or search for the app and install it manually.
     </string>
+    <!-- Title for an app which is being installed. -->
+    <string name="app_installing_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> installing, <xliff:g id="progress" example="30%">%2$s</xliff:g> complete</string>
     <!-- Title for an app which is being downloaded. -->
     <string name="app_downloading_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> downloading, <xliff:g id="progress" example="30%">%2$s</xliff:g> complete</string>
     <!-- Title for an app whose download has been started. -->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index dc7182f..adc2238 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -35,12 +35,12 @@
         <item name="allAppsInterimScrimAlpha">46</item>
         <item name="allAppsNavBarScrimColor">#66FFFFFF</item>
         <item name="allAppsTheme">@style/AllAppsTheme</item>
-        <item name="popupColorPrimary">#FFF</item>
-        <item name="popupColorSecondary">#F1F3F4</item>
-        <item name="popupColorTertiary">#E0E0E0</item> <!-- Gray 300 -->
+        <item name="popupColorPrimary">@color/popup_color_primary_light</item>
+        <item name="popupColorSecondary">@color/popup_color_secondary_light</item>
+        <item name="popupColorTertiary">@color/popup_color_tertiary_light</item>
         <item name="isMainColorDark">false</item>
         <item name="isWorkspaceDarkText">false</item>
-        <item name="workspaceTextColor">@android:color/white</item>
+        <item name="workspaceTextColor">@color/workspace_text_color_light</item>
         <item name="workspaceShadowColor">#B0000000</item>
         <item name="workspaceAmbientShadowColor">#33000000</item>
         <item name="workspaceKeyShadowColor">#44000000</item>
@@ -74,7 +74,7 @@
     </style>
 
     <style name="LauncherTheme.DarkText" parent="@style/LauncherTheme">
-        <item name="workspaceTextColor">#FF212121</item>
+        <item name="workspaceTextColor">@color/workspace_text_color_dark</item>
         <item name="allAppsInterimScrimAlpha">128</item>
         <item name="workspaceShadowColor">@android:color/transparent</item>
         <item name="workspaceAmbientShadowColor">@android:color/transparent</item>
@@ -88,9 +88,9 @@
     </style>
 
     <style name="LauncherTheme.Dark" parent="@style/LauncherTheme">
-        <item name="android:textColorPrimary">#FFFFFFFF</item>
-        <item name="android:textColorSecondary">#FFFFFFFF</item>
-        <item name="android:textColorTertiary">#CCFFFFFF</item>
+        <item name="android:textColorPrimary">@color/text_color_primary_dark</item>
+        <item name="android:textColorSecondary">@color/text_color_secondary_dark</item>
+        <item name="android:textColorTertiary">@color/text_color_tertiary_dark</item>
         <item name="android:textColorHint">#A0FFFFFF</item>
         <item name="android:colorControlHighlight">#A0FFFFFF</item>
         <item name="android:colorPrimary">#FF212121</item>
@@ -98,9 +98,9 @@
         <item name="allAppsInterimScrimAlpha">102</item>
         <item name="allAppsNavBarScrimColor">#80000000</item>
         <item name="allAppsTheme">@style/AllAppsTheme.Dark</item>
-        <item name="popupColorPrimary">#3C4043</item> <!-- Gray 800 -->
-        <item name="popupColorSecondary">#202124</item>
-        <item name="popupColorTertiary">#757575</item> <!-- Gray 600 -->
+        <item name="popupColorPrimary">@color/popup_color_primary_dark</item>
+        <item name="popupColorSecondary">@color/popup_color_secondary_dark</item>
+        <item name="popupColorTertiary">@color/popup_color_tertiary_dark</item>
         <item name="widgetsTheme">@style/WidgetContainerTheme.Dark</item>
         <item name="folderDotColor">?android:attr/colorPrimary</item>
         <item name="folderFillColor">?android:attr/colorBackground</item>
@@ -125,7 +125,7 @@
         <item name="allAppsInterimScrimAlpha">25</item>
         <item name="folderFillColor">#CDFFFFFF</item>
         <item name="folderTextColor">?attr/workspaceTextColor</item>
-        <item name="workspaceTextColor">#FF212121</item>
+        <item name="workspaceTextColor">@color/workspace_text_color_dark</item>
         <item name="workspaceShadowColor">@android:color/transparent</item>
         <item name="workspaceAmbientShadowColor">@android:color/transparent</item>
         <item name="workspaceKeyShadowColor">@android:color/transparent</item>
diff --git a/res/xml/size_limits.xml b/res/xml/size_limits.xml
new file mode 100644
index 0000000..ba57014
--- /dev/null
+++ b/res/xml/size_limits.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2021 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.
+-->
+
+<device-paddings xmlns:launcher="http://schemas.android.com/apk/res-auto" >
+
+    <device-padding
+        launcher:maxEmptySpace="88dp">
+        <workspaceTopPadding
+            launcher:a="0"
+            launcher:b="0"/>
+        <workspaceBottomPadding
+            launcher:a="0.52"
+            launcher:b="0"/>
+        <hotseatBottomPadding
+            launcher:a="0.48"
+            launcher:b="0"/>
+    </device-padding>
+
+    <device-padding
+        launcher:maxEmptySpace="97dp">
+        <workspaceTopPadding
+            launcher:a="0"
+            launcher:b="16dp"/>
+        <workspaceBottomPadding
+            launcher:a="0.50"
+            launcher:b="0"
+            launcher:c="-16dp"/>
+        <hotseatBottomPadding
+            launcher:a="0.50"
+            launcher:b="0"
+            launcher:c="16dp"/>
+    </device-padding>
+
+    <device-padding
+        launcher:maxEmptySpace="107dp">
+        <workspaceTopPadding
+            launcher:a="0"
+            launcher:b="16dp"/>
+        <workspaceBottomPadding
+            launcher:a="0"
+            launcher:b="36dp"/>
+        <hotseatBottomPadding
+            launcher:a="1"
+            launcher:b="0"
+            launcher:c="52dp"/>
+    </device-padding>
+
+    <device-padding
+        launcher:maxEmptySpace="9999dp">
+        <workspaceTopPadding
+            launcher:a="0.38"
+            launcher:c="36dp"/>
+        <workspaceBottomPadding
+            launcher:a="0.62"
+            launcher:c="36dp"/>
+        <hotseatBottomPadding
+            launcher:a="0"
+            launcher:b="36dp"/>
+    </device-padding>
+
+</device-paddings>
\ No newline at end of file
diff --git a/robolectric_tests/Android.bp b/robolectric_tests/Android.bp
new file mode 100644
index 0000000..c738df9
--- /dev/null
+++ b/robolectric_tests/Android.bp
@@ -0,0 +1,46 @@
+// Copyright (C) 2021 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.
+
+//
+// Launcher Robolectric test target.
+//
+//        "robolectric_android-all-stub", not needed, we write our own stubs
+android_robolectric_test {
+    name: "LauncherRoboTests",
+    srcs: [
+        "src/**/*.java",
+    ],
+    java_resource_dirs: [
+        "resources",
+        "res",
+        "config",
+    ],
+    static_libs: [
+        "truth-prebuilt",
+        "Launcher3TestCommon",
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "mockito-robolectric-prebuilt",
+    ],
+    //robolectric_prebuilt_version: "4.4",
+    libs: [
+        "platform-robolectric-4.4-prebuilt",
+    ],
+    instrumentation_for: "Launcher3",
+
+    test_options: {
+        timeout: 36000,
+    },
+}
+
diff --git a/robolectric_tests/Android.mk b/robolectric_tests/Android.mk
deleted file mode 100644
index 836ded5..0000000
--- a/robolectric_tests/Android.mk
+++ /dev/null
@@ -1,62 +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.
-
-#############################################
-# Launcher Robolectric test target.         #
-#############################################
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := LauncherRoboTests
-LOCAL_MODULE_CLASS := JAVA_LIBRARIES
-
-LOCAL_SDK_VERSION := system_current
-LOCAL_SRC_FILES := \
-	$(call all-java-files-under, src) \
-	$(call all-java-files-under, ../tests/src_common)
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
-    androidx.test.runner \
-    androidx.test.rules \
-    mockito-robolectric-prebuilt \
-    truth-prebuilt
-LOCAL_JAVA_LIBRARIES := \
-    platform-robolectric-4.3.1-prebuilt
-
-LOCAL_JAVA_RESOURCE_DIRS := resources config
-
-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)
-
-############################################
-# Target to run the previous target.       #
-############################################
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := RunLauncherRoboTests
-LOCAL_SDK_VERSION := system_current
-LOCAL_JAVA_LIBRARIES := LauncherRoboTests
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-LOCAL_TEST_PACKAGE := Launcher3
-LOCAL_INSTRUMENT_SOURCE_DIRS := packages/apps/Launcher3/src
-
-LOCAL_ROBOTEST_TIMEOUT := 36000
-
-include prebuilts/misc/common/robolectric/4.4/run_robotests.mk
diff --git a/robolectric_tests/config/robolectric.properties b/robolectric_tests/config/robolectric.properties
index 4e811f3..1b170e1 100644
--- a/robolectric_tests/config/robolectric.properties
+++ b/robolectric_tests/config/robolectric.properties
@@ -1,4 +1,5 @@
 sdk=29
+
 shadows= \
     com.android.launcher3.shadows.LShadowAppPredictionManager \
     com.android.launcher3.shadows.LShadowAppWidgetManager \
@@ -11,4 +12,4 @@
     com.android.launcher3.shadows.ShadowOverrides \
     com.android.launcher3.shadows.ShadowSurfaceTransactionApplier \
 
-application=com.android.launcher3.util.LauncherTestApplication
\ No newline at end of file
+application=com.android.launcher3.util.LauncherTestApplication
diff --git a/robolectric_tests/resources/widgets_predication_update_task_data.txt b/robolectric_tests/resources/widgets_predication_update_task_data.txt
new file mode 100644
index 0000000..941d195
--- /dev/null
+++ b/robolectric_tests/resources/widgets_predication_update_task_data.txt
@@ -0,0 +1,24 @@
+# Model data used by WidgetsPredictionUpdateTasksTest
+
+classMap s com.android.launcher3.model.data.WorkspaceItemInfo
+classMap w com.android.launcher3.model.data.LauncherAppWidgetInfo
+
+# Items for the BgDataModel
+
+# App shortcuts
+bgItem s itemType=0 title=app1-class1 intent=component=app1/class1 id=1
+bgItem s itemType=0 title=app1-class2 intent=component=app1/class2 id=2
+bgItem s itemType=0 title=app2-class1 intent=component=app2/class1 id=3
+bgItem s itemType=0 title=app2-class2 intent=component=app2/class2 id=4
+
+# Promise icons for app3
+bgItem s itemType=0 status=2 title=app3-class1 intent=component=app3/class1 id=5
+bgItem s itemType=0 status=2 title=app3-class2 intent=component=app3/class2 id=6
+bgItem s itemType=1 status=1 title=app3-shrt intent=component=app3/class3 id=7
+
+# Promise icon for app4
+bgItem s itemType=1 status=1 title=app4-shrt intent=component=app4/class1 id=8
+
+# Widget
+bgItem w providerName=app4/provider1 id=9
+bgItem w providerName=app5/provider1 id=10
\ No newline at end of file
diff --git a/robolectric_tests/src/com/android/launcher3/testing/TestActivity.java b/robolectric_tests/src/com/android/launcher3/testing/TestActivity.java
new file mode 100644
index 0000000..dbf4b3e
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/testing/TestActivity.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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 com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.BaseDragLayer;
+
+/** An empty activity for {@link android.app.Fragment}s, {@link android.view.View}s testing. */
+public class TestActivity extends BaseActivity implements ActivityContext {
+
+    private DeviceProfile mDeviceProfile;
+
+    @Override
+    public BaseDragLayer getDragLayer() {
+        return null;
+    }
+
+    @Override
+    public DeviceProfile getDeviceProfile() {
+        return mDeviceProfile;
+    }
+
+    public void setDeviceProfile(DeviceProfile deviceProfile) {
+        mDeviceProfile = deviceProfile;
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java b/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java
index 957ae77..34cb2ad 100644
--- a/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java
+++ b/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java
@@ -38,7 +38,7 @@
 import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.LauncherLayoutBuilder.FolderBuilder;
 import com.android.launcher3.util.LauncherModelHelper;
-import com.android.launcher3.widget.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/robolectric_tests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java b/robolectric_tests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java
new file mode 100644
index 0000000..92f77f2
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2021 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.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.graphics.Point;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public final class LauncherAppWidgetProviderInfoTest {
+
+    private static final int CELL_SIZE = 50;
+
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+    }
+
+    @Test
+    public void initSpans_minWidthSmallerThanCellWidth_shouldInitializeSpansToOne() {
+        LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+        info.minWidth = 20;
+        info.minHeight = 20;
+        InvariantDeviceProfile idp = createIDP();
+
+        info.initSpans(mContext, idp);
+
+        assertThat(info.spanX).isEqualTo(1);
+        assertThat(info.spanY).isEqualTo(1);
+    }
+
+    @Test
+    public void initSpans_minWidthLargerThanCellWidth_shouldInitializeSpans() {
+        LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+        info.minWidth = 80;
+        info.minHeight = 80;
+        InvariantDeviceProfile idp = createIDP();
+
+        info.initSpans(mContext, idp);
+
+        assertThat(info.spanX).isEqualTo(2);
+        assertThat(info.spanY).isEqualTo(2);
+    }
+
+    @Test
+    public void initSpans_minResizeWidthUnspecified_shouldInitializeMinSpansToOne() {
+        LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+        InvariantDeviceProfile idp = createIDP();
+
+        info.initSpans(mContext, idp);
+
+        assertThat(info.minSpanX).isEqualTo(1);
+        assertThat(info.minSpanY).isEqualTo(1);
+    }
+
+    @Test
+    public void initSpans_minResizeWidthSmallerThanCellWidth_shouldInitializeMinSpansToOne() {
+        LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+        info.minResizeWidth = 20;
+        info.minResizeHeight = 20;
+        InvariantDeviceProfile idp = createIDP();
+
+        info.initSpans(mContext, idp);
+
+        assertThat(info.minSpanX).isEqualTo(1);
+        assertThat(info.minSpanY).isEqualTo(1);
+    }
+
+    @Test
+    public void initSpans_minResizeWidthLargerThanCellWidth_shouldInitializeMinSpans() {
+        LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+        info.minResizeWidth = 80;
+        info.minResizeHeight = 80;
+        InvariantDeviceProfile idp = createIDP();
+
+        info.initSpans(mContext, idp);
+
+        assertThat(info.minSpanX).isEqualTo(2);
+        assertThat(info.minSpanY).isEqualTo(2);
+    }
+
+    private InvariantDeviceProfile createIDP() {
+        DeviceProfile profile = Mockito.mock(DeviceProfile.class);
+        Mockito.when(profile.getCellSize()).thenReturn(new Point(CELL_SIZE, CELL_SIZE));
+
+        InvariantDeviceProfile idp = new InvariantDeviceProfile();
+        idp.landscapeProfile = idp.portraitProfile = profile;
+        return idp;
+    }
+
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
deleted file mode 100644
index 5ab3106..0000000
--- a/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.widget;
-
-import static org.mockito.Matchers.eq;
-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 com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.WidgetPreviewLoader;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.model.data.PackageItemInfo;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.shadows.ShadowPackageManager;
-import org.robolectric.util.ReflectionHelpers;
-
-import java.util.ArrayList;
-import java.util.Collections;
-
-@RunWith(RobolectricTestRunner.class)
-public class WidgetsListAdapterTest {
-
-    @Mock private LayoutInflater mMockLayoutInflater;
-    @Mock private WidgetPreviewLoader mMockWidgetCache;
-    @Mock private RecyclerView.AdapterDataObserver mListener;
-    @Mock private IconCache mIconCache;
-
-    private WidgetsListAdapter mAdapter;
-    private InvariantDeviceProfile mTestProfile;
-    private Context mContext;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mContext = RuntimeEnvironment.application;
-        mTestProfile = new InvariantDeviceProfile();
-        mTestProfile.numRows = 5;
-        mTestProfile.numColumns = 5;
-        mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, mMockWidgetCache,
-                mIconCache, null, null);
-        mAdapter.registerAdapterDataObserver(mListener);
-    }
-
-    @Test
-    public void test_notifyDataSetChanged() throws Exception {
-        mAdapter.setWidgets(generateSampleMap(1));
-        verify(mListener, times(1)).onChanged();
-    }
-
-    @Test
-    public void test_notifyItemInserted() throws Exception {
-        mAdapter.setWidgets(generateSampleMap(1));
-        mAdapter.setWidgets(generateSampleMap(2));
-        verify(mListener, times(1)).onChanged();
-        verify(mListener, times(1)).onItemRangeInserted(eq(1), eq(1));
-    }
-
-    @Test
-    public void test_notifyItemRemoved() throws Exception {
-        mAdapter.setWidgets(generateSampleMap(2));
-        mAdapter.setWidgets(generateSampleMap(1));
-        verify(mListener, times(1)).onChanged();
-        verify(mListener, times(1)).onItemRangeRemoved(eq(1), eq(1));
-    }
-
-    @Test
-    public void testNotifyItemChanged_PackageIconDiff() throws Exception {
-        mAdapter.setWidgets(generateSampleMap(1));
-        mAdapter.setWidgets(generateSampleMap(1));
-        verify(mListener, times(1)).onChanged();
-        verify(mListener, times(1)).onItemRangeChanged(eq(0), eq(1), isNull());
-    }
-
-    @Test
-    public void testNotifyItemChanged_widgetItemInfoDiff() throws Exception {
-        // TODO: same package name but item number changed
-    }
-
-    @Test
-    public void testNotifyItemInsertedRemoved_hodgepodge() throws Exception {
-        // TODO: insert and remove combined.          curMap
-        // newMap [A, C, D]                           [A, B, E]
-        // B - C < 0, removed B from index 1          [A, E]
-        // E - C > 0, C inserted to index 1           [A, C, E]
-        // E - D > 0, D inserted to index 2           [A, C, D, E]
-        // E - null = -1, E deleted from index 3      [A, C, D]
-    }
-
-    /**
-     * 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
-     */
-    private ArrayList<WidgetListRowEntry> generateSampleMap(int num) {
-        ArrayList<WidgetListRowEntry> result = new ArrayList<>();
-        if (num <= 0) return result;
-        ShadowPackageManager spm = shadowOf(mContext.getPackageManager());
-
-        for (int i = 0; i < num; i++) {
-            ComponentName cn = new ComponentName("com.placeholder.apk" + i, "PlaceholderWidet");
-
-            AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
-            widgetInfo.provider = cn;
-            ReflectionHelpers.setField(widgetInfo, "providerInfo", spm.addReceiverIfNotPresent(cn));
-
-            WidgetItem wi = new WidgetItem(LauncherAppWidgetProviderInfo
-                    .fromProviderInfo(mContext, widgetInfo), mTestProfile, mIconCache);
-
-            PackageItemInfo pInfo = new PackageItemInfo(wi.componentName.getPackageName());
-            pInfo.title = pInfo.packageName;
-            pInfo.user = wi.user;
-            pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
-
-            result.add(new WidgetListRowEntry(pInfo, new ArrayList<>(Collections.singleton(wi))));
-        }
-
-        return result;
-    }
-}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
new file mode 100644
index 0000000..b972c6f
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.UserHandle;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.picker.WidgetsListAdapter.WidgetListBaseRowEntryComparator;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsDiffReporterTest {
+    private static final String TEST_PACKAGE_PREFIX = "com.google.test";
+    private static final WidgetListBaseRowEntryComparator COMPARATOR =
+            new WidgetListBaseRowEntryComparator();
+
+    @Mock private IconCache mIconCache;
+    @Mock private RecyclerView.Adapter mAdapter;
+
+    private InvariantDeviceProfile mTestProfile;
+    private WidgetsDiffReporter mWidgetsDiffReporter;
+    private Context mContext;
+    private WidgetsListHeaderEntry mHeaderA;
+    private WidgetsListHeaderEntry mHeaderB;
+    private WidgetsListHeaderEntry mHeaderC;
+    private WidgetsListHeaderEntry mHeaderD;
+    private WidgetsListHeaderEntry mHeaderE;
+    private WidgetsListContentEntry mContentC;
+    private WidgetsListContentEntry mContentE;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mTestProfile = new InvariantDeviceProfile();
+        mTestProfile.numRows = 5;
+        mTestProfile.numColumns = 5;
+
+        doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0))
+                .getComponent().getPackageName())
+                .when(mIconCache).getTitleNoCache(any());
+
+        mContext = RuntimeEnvironment.application;
+        mWidgetsDiffReporter = new WidgetsDiffReporter(mIconCache, mAdapter);
+        mHeaderA = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "A",
+                /* appName= */ "A", /* numOfWidgets= */ 3);
+        mHeaderB = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "B",
+                /* appName= */ "B", /* numOfWidgets= */ 3);
+        mHeaderC = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "C",
+                /* appName= */ "C", /* numOfWidgets= */ 3);
+        mContentC = createWidgetsContentEntry(TEST_PACKAGE_PREFIX + "C",
+                /* appName= */ "C", /* numOfWidgets= */ 3);
+        mHeaderD = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "D",
+                /* appName= */ "D", /* numOfWidgets= */ 3);
+        mHeaderE = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "E",
+                /* appName= */ "E", /* numOfWidgets= */ 3);
+        mContentE = createWidgetsContentEntry(TEST_PACKAGE_PREFIX + "E",
+                /* appName= */ "E", /* numOfWidgets= */ 3);
+    }
+
+    @Test
+    public void listNotChanged_shouldNotInvokeAnyCallbacks() {
+        // GIVEN the current list has app headers [A, B, C].
+        ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
+                List.of(mHeaderA, mHeaderB, mHeaderC));
+
+        // WHEN computing the list difference.
+        mWidgetsDiffReporter.process(currentList, currentList, COMPARATOR);
+
+        // THEN there is no adaptor callback.
+        verifyZeroInteractions(mAdapter);
+        // THEN the current list contains the same entries.
+        assertThat(currentList).containsExactly(mHeaderA, mHeaderB, mHeaderC);
+    }
+
+    @Test
+    public void headersOnly_emptyListToNonEmpty_shouldInvokeNotifyDataSetChanged() {
+        // GIVEN the current list has app headers [A, B, C].
+        ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>();
+
+        List<WidgetsListBaseEntry> newList = List.of(
+                createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "A", "A", 3),
+                createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "B", "B", 3),
+                createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "C", "C", 3));
+
+        // WHEN computing the list difference.
+        mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
+
+        // THEN notifyDataSetChanged is called
+        verify(mAdapter).notifyDataSetChanged();
+        // THEN the current list contains all elements from the new list.
+        assertThat(currentList).containsExactlyElementsIn(newList);
+    }
+
+    @Test
+    public void headersOnly_nonEmptyToEmptyList_shouldInvokeNotifyDataSetChanged() {
+        // GIVEN the current list has app headers [A, B, C].
+        ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
+                List.of(mHeaderA, mHeaderB, mHeaderC));
+        // GIVEN the new list is empty.
+        List<WidgetsListBaseEntry> newList = List.of();
+
+        // WHEN computing the list difference.
+        mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
+
+        // THEN notifyDataSetChanged is called.
+        verify(mAdapter).notifyDataSetChanged();
+        // THEN the current list isEmpty.
+        assertThat(currentList).isEmpty();
+    }
+
+    @Test
+    public void headersOnly_itemAddedAndRemovedInTheNewList_shouldInvokeCorrectCallbacks() {
+        // GIVEN the current list has app headers [A, B, D].
+        ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
+                List.of(mHeaderA, mHeaderB, mHeaderD));
+        // GIVEN the new list has app headers [A, C, E].
+        List<WidgetsListBaseEntry> newList = List.of(mHeaderA, mHeaderC, mHeaderE);
+
+        // WHEN computing the list difference.
+        mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
+
+        // THEN "B" is removed from position 1.
+        verify(mAdapter).notifyItemRemoved(/* position= */ 1);
+        // THEN "D" is removed from position 2.
+        verify(mAdapter).notifyItemRemoved(/* position= */ 2);
+        // THEN "C" is inserted at position 1.
+        verify(mAdapter).notifyItemInserted(/* position= */ 1);
+        // THEN "E" is inserted at position 2.
+        verify(mAdapter).notifyItemInserted(/* position= */ 2);
+        // THEN the current list contains all elements from the new list.
+        assertThat(currentList).containsExactlyElementsIn(newList);
+    }
+
+    @Test
+    public void headersContentsMix_itemAddedAndRemovedInTheNewList_shouldInvokeCorrectCallbacks() {
+        // GIVEN the current list has app headers [A, B, E content].
+        ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
+                List.of(mHeaderA, mHeaderB, mContentE));
+        // GIVEN the new list has app headers [A, C content, D].
+        List<WidgetsListBaseEntry> newList = List.of(mHeaderA, mContentC, mHeaderD);
+
+        // WHEN computing the list difference.
+        mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
+
+        // THEN "B" is removed from position 1.
+        verify(mAdapter).notifyItemRemoved(/* position= */ 1);
+        // THEN "C content" is inserted at position 1.
+        verify(mAdapter).notifyItemInserted(/* position= */ 1);
+        // THEN "D" is inserted at position 2.
+        verify(mAdapter).notifyItemInserted(/* position= */ 2);
+        // THEN "E content" is removed from position 3.
+        verify(mAdapter).notifyItemRemoved(/* position= */ 3);
+        // THEN the current list contains all elements from the new list.
+        assertThat(currentList).containsExactlyElementsIn(newList);
+    }
+
+    @Test
+    public void headersContentsMix_userInteractWithHeader_shouldInvokeCorrectCallbacks() {
+        // GIVEN the current list has app headers [A, B, E content].
+        ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(
+                List.of(mHeaderA, mHeaderB, mContentE));
+        // GIVEN the new list has app headers [A, B, E content].
+        List<WidgetsListBaseEntry> newList = List.of(mHeaderA, mHeaderB, mContentE);
+        // GIVEN the user has interacted with B.
+        mHeaderB.setIsWidgetListShown(true);
+
+        // WHEN computing the list difference.
+        mWidgetsDiffReporter.process(currentList, newList, COMPARATOR);
+
+        // THEN notify "B" has been changed.
+        verify(mAdapter).notifyItemChanged(/* position= */ 1);
+        // THEN the current list contains all elements from the new list.
+        assertThat(currentList).containsExactlyElementsIn(newList);
+    }
+
+
+    private WidgetsListHeaderEntry createWidgetsHeaderEntry(String packageName, String appName,
+            int numOfWidgets) {
+        List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets);
+        PackageItemInfo pInfo = createPackageItemInfo(packageName, appName,
+                widgetItems.get(0).user);
+
+        return new WidgetsListHeaderEntry(pInfo, /* titleSectionName= */ "", widgetItems);
+    }
+
+    private WidgetsListContentEntry createWidgetsContentEntry(String packageName, String appName,
+            int numOfWidgets) {
+        List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets);
+        PackageItemInfo pInfo = createPackageItemInfo(packageName, appName,
+                widgetItems.get(0).user);
+
+        return new WidgetsListContentEntry(pInfo, /* titleSectionName= */ "", widgetItems);
+    }
+
+    private PackageItemInfo createPackageItemInfo(String packageName, String appName,
+            UserHandle userHandle) {
+        PackageItemInfo pInfo = new PackageItemInfo(packageName);
+        pInfo.title = appName;
+        pInfo.user = userHandle;
+        pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+        return pInfo;
+    }
+
+    private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+        ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+        ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+        for (int i = 0; i < numOfWidgets; i++) {
+            ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
+            AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+            widgetInfo.provider = cn;
+            ReflectionHelpers.setField(widgetInfo, "providerInfo",
+                    packageManager.addReceiverIfNotPresent(cn));
+
+            WidgetItem widgetItem = new WidgetItem(
+                    LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
+                    mTestProfile, mIconCache);
+            widgetItems.add(widgetItem);
+        }
+        return widgetItems;
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
new file mode 100644
index 0000000..a7c8d92
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.doAnswer;
+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 com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.WidgetPreviewLoader;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsListAdapterTest {
+    private static final String TEST_PACKAGE_PLACEHOLDER = "com.google.test";
+
+    @Mock private LayoutInflater mMockLayoutInflater;
+    @Mock private WidgetPreviewLoader mMockWidgetCache;
+    @Mock private RecyclerView.AdapterDataObserver mListener;
+    @Mock private IconCache mIconCache;
+
+    private WidgetsListAdapter mAdapter;
+    private InvariantDeviceProfile mTestProfile;
+    private Context mContext;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mTestProfile = new InvariantDeviceProfile();
+        mTestProfile.numRows = 5;
+        mTestProfile.numColumns = 5;
+        mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, mMockWidgetCache,
+                mIconCache, null, null);
+        mAdapter.registerAdapterDataObserver(mListener);
+
+        doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0))
+                        .getComponent().getPackageName())
+                .when(mIconCache).getTitleNoCache(any());
+    }
+
+    @Test
+    public void setWidgets_shouldNotifyDataSetChanged() {
+        mAdapter.setWidgets(generateSampleMap(1));
+
+        verify(mListener).onChanged();
+    }
+
+    @Test
+    public void setWidgets_withItemInserted_shouldNotifyItemInserted() {
+        mAdapter.setWidgets(generateSampleMap(1));
+        mAdapter.setWidgets(generateSampleMap(2));
+
+        verify(mListener).onItemRangeInserted(eq(1), eq(1));
+    }
+
+    @Test
+    public void setWidgets_withItemRemoved_shouldNotifyItemRemoved() {
+        mAdapter.setWidgets(generateSampleMap(2));
+        mAdapter.setWidgets(generateSampleMap(1));
+
+        verify(mListener).onItemRangeRemoved(eq(1), eq(1));
+    }
+
+    @Test
+    public void setWidgets_appIconChanged_shouldNotifyItemChanged() {
+        mAdapter.setWidgets(generateSampleMap(1));
+        mAdapter.setWidgets(generateSampleMap(1));
+
+        verify(mListener).onItemRangeChanged(eq(0), eq(1), isNull());
+    }
+
+    @Test
+    public void headerClick_expanded_shouldNotifyItemChange() {
+        // GIVEN a list of widgets entries:
+        // [com.google.test0, com.google.test0 content,
+        //  com.google.test1, com.google.test1 content,
+        //  com.google.test2, com.google.test2 content]
+        // The visible widgets entries: [com.google.test0, com.google.test1, com.google.test2].
+        mAdapter.setWidgets(generateSampleMap(3));
+
+        // WHEN com.google.test.1 header is expanded.
+        mAdapter.onHeaderClicked(/* isExpanded= */ true, TEST_PACKAGE_PLACEHOLDER + 1);
+
+        // THEN the visible entries list becomes:
+        // [com.google.test0, com.google.test1, com.google.test1 content, com.google.test2]
+        // com.google.test.1 content is inserted into position 2.
+        verify(mListener).onItemRangeInserted(eq(2), eq(1));
+    }
+
+    @Test
+    public void setWidgets_expandedApp_moreWidgets_shouldNotifyItemChangedWithWidgetItemInfoDiff() {
+        // GIVEN the adapter was first populated with com.google.test0 & com.google.test1. Each app
+        // has one widget.
+        ArrayList<WidgetsListBaseEntry> allEntries = generateSampleMap(2);
+        mAdapter.setWidgets(allEntries);
+        // GIVEN test com.google.test1 is expanded.
+        // Visible entries in the adapter are:
+        // [com.google.test0, com.google.test1, com.google.test1 content]
+        mAdapter.onHeaderClicked(/* isExpanded= */ true, TEST_PACKAGE_PLACEHOLDER + 1);
+        Mockito.reset(mListener);
+
+        // WHEN the adapter is updated with the same list of apps but com.google.test1 has 2 widgets
+        // now.
+        WidgetsListContentEntry testPackage1ContentEntry =
+                (WidgetsListContentEntry) allEntries.get(3);
+        WidgetItem widgetItem = testPackage1ContentEntry.mWidgets.get(0);
+        WidgetsListContentEntry newTestPackage1ContentEntry = new WidgetsListContentEntry(
+                testPackage1ContentEntry.mPkgItem,
+                testPackage1ContentEntry.mTitleSectionName, List.of(widgetItem, widgetItem));
+        allEntries.set(3, newTestPackage1ContentEntry);
+        mAdapter.setWidgets(allEntries);
+
+        // THEN the onItemRangeChanged is invoked for "com.google.test1 content" at index 2.
+        verify(mListener).onItemRangeChanged(eq(2), eq(1), isNull());
+    }
+
+    @Test
+    public void setWidgets_hodgepodge_shouldInvokeExpectedDataObserverCallbacks() {
+        // GIVEN a widgets entry list:
+        // Index:  0|   1      | 2|      3   | 4|     5    | 6|     7    | 8|     9    |
+        //        [A, A content, B, B content, C, C content, D, D content, E, E content]
+        List<WidgetsListBaseEntry> allAppsWithWidgets = generateSampleMap(5);
+        // GIVEN the current widgets list consist of [A, A content, B, B content, E, E content].
+        // GIVEN the visible widgets list consist of [A, B, E]
+        List<WidgetsListBaseEntry> currentList = List.of(
+                // A & A content
+                allAppsWithWidgets.get(0), allAppsWithWidgets.get(1),
+                // B & B content
+                allAppsWithWidgets.get(2), allAppsWithWidgets.get(3),
+                // E & E content
+                allAppsWithWidgets.get(8), allAppsWithWidgets.get(9));
+        mAdapter.setWidgets(currentList);
+
+        // WHEN the widgets list is updated to [A, A content, C, C content, D, D content].
+        // WHEN the visible widgets list is updated to [A, C, D].
+        List<WidgetsListBaseEntry> newList = List.of(
+                // A & A content
+                allAppsWithWidgets.get(0), allAppsWithWidgets.get(1),
+                // C & C content
+                allAppsWithWidgets.get(4), allAppsWithWidgets.get(5),
+                // D & D content
+                allAppsWithWidgets.get(6), allAppsWithWidgets.get(7));
+        mAdapter.setWidgets(newList);
+
+        // Computation logic                           | [Intermediate list during computation]
+        // THEN B <> C < 0, removed B from index 1     | [A, E]
+        verify(mListener).onItemRangeRemoved(/* positionStart= */ 1, /* itemCount= */ 1);
+        // THEN E <> C > 0, C inserted to index 1      | [A, C, E]
+        verify(mListener).onItemRangeInserted(/* positionStart= */ 1, /* itemCount= */ 1);
+        // THEN E <> D > 0, D inserted to index 2      | [A, C, D, E]
+        verify(mListener).onItemRangeInserted(/* positionStart= */ 2, /* itemCount= */ 1);
+        // THEN E <> null = -1, E deleted from index 3 | [A, C, D]
+        verify(mListener).onItemRangeRemoved(/* positionStart= */ 3, /* itemCount= */ 1);
+    }
+
+    /**
+     * Generates a list of sample widget entries.
+     *
+     * <p>Each sample app has 1 widget only. An app is represented by 2 entries,
+     * {@link WidgetsListHeaderEntry} & {@link WidgetsListContentEntry}. Only
+     * {@link WidgetsListHeaderEntry} is always visible in the {@link WidgetsListAdapter}.
+     * {@link WidgetsListContentEntry} is only shown upon clicking the corresponding app's
+     * {@link WidgetsListHeaderEntry}. Only at most one {@link WidgetsListContentEntry} is shown at
+     * a time.
+     *
+     * @param num the number of apps that have widgets.
+     */
+    private ArrayList<WidgetsListBaseEntry> generateSampleMap(int num) {
+        ArrayList<WidgetsListBaseEntry> result = new ArrayList<>();
+        if (num <= 0) return result;
+
+        for (int i = 0; i < num; i++) {
+            String packageName = TEST_PACKAGE_PLACEHOLDER + i;
+
+            List<WidgetItem> widgetItems = generateWidgetItems(packageName, /* numOfWidgets= */ 1);
+
+            PackageItemInfo pInfo = new PackageItemInfo(packageName);
+            pInfo.title = pInfo.packageName;
+            pInfo.user = widgetItems.get(0).user;
+            pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+
+            result.add(new WidgetsListHeaderEntry(pInfo, /* titleSectionName= */ "", widgetItems));
+            result.add(new WidgetsListContentEntry(pInfo, /* titleSectionName= */ "", widgetItems));
+        }
+
+        return result;
+    }
+
+    private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+        ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+        ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+        for (int i = 0; i < numOfWidgets; i++) {
+            ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
+            AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+            widgetInfo.provider = cn;
+            ReflectionHelpers.setField(widgetInfo, "providerInfo",
+                    packageManager.addReceiverIfNotPresent(cn));
+
+            widgetItems.add(new WidgetItem(
+                    LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
+                    mTestProfile, mIconCache));
+        }
+        return widgetItems;
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
new file mode 100644
index 0000000..848630e
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+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 android.view.View;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.picker.WidgetsListHeaderViewHolderBinder.OnHeaderClickListener;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsListHeaderViewHolderBinderTest {
+    private static final String TEST_PACKAGE = "com.google.test";
+    private static final String APP_NAME = "Test app";
+
+    private Context mContext;
+    private WidgetsListHeaderViewHolderBinder mViewHolderBinder;
+    private InvariantDeviceProfile mTestProfile;
+    // Replace ActivityController with ActivityScenario, which is the recommended way for activity
+    // testing.
+    private ActivityController<TestActivity> mActivityController;
+    private TestActivity mTestActivity;
+    private FakeOnHeaderClickListener mFakeOnHeaderClickListener = new FakeOnHeaderClickListener();
+
+    @Mock
+    private IconCache mIconCache;
+    @Mock
+    private DeviceProfile mDeviceProfile;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mTestProfile = new InvariantDeviceProfile();
+        mTestProfile.numRows = 5;
+        mTestProfile.numColumns = 5;
+
+        mActivityController = Robolectric.buildActivity(TestActivity.class);
+        mTestActivity = mActivityController.setup().get();
+        mTestActivity.setDeviceProfile(mDeviceProfile);
+
+        doAnswer(invocation -> {
+            ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+            return componentWithLabel.getComponent().getShortClassName();
+        }).when(mIconCache).getTitleNoCache(any());
+
+        mViewHolderBinder = new WidgetsListHeaderViewHolderBinder(
+                LayoutInflater.from(mTestActivity),
+                mFakeOnHeaderClickListener);
+    }
+
+    @After
+    public void tearDown() {
+        mActivityController.destroy();
+    }
+
+    @Test
+    public void bindViewHolder_appWith3Widgets_shouldShowTheCorrectAppNameAndSubtitle() {
+        WidgetsListHeaderHolder viewHolder = mViewHolderBinder.newViewHolder(
+                new FrameLayout(mTestActivity));
+        WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+        WidgetsListHeaderEntry entry = generateSampleAppHeader(
+                APP_NAME,
+                TEST_PACKAGE,
+                /* numOfWidgets= */ 3);
+        mViewHolderBinder.bindViewHolder(viewHolder, entry);
+
+        TextView appTitle = widgetsListHeader.findViewById(R.id.app_title);
+        TextView appSubtitle = widgetsListHeader.findViewById(R.id.app_subtitle);
+        assertThat(appTitle.getText()).isEqualTo(APP_NAME);
+        assertThat(appSubtitle.getText()).isEqualTo("3 widgets");
+    }
+
+    private WidgetsListHeaderEntry generateSampleAppHeader(String appName, String packageName,
+            int numOfWidgets) {
+        PackageItemInfo appInfo = new PackageItemInfo(packageName);
+        appInfo.title = appName;
+        appInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+
+        return new WidgetsListHeaderEntry(appInfo,
+                /* titleSectionName= */ "",
+                generateWidgetItems(packageName, numOfWidgets));
+    }
+
+    private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+        ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+        ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+        for (int i = 0; i < numOfWidgets; i++) {
+            ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
+            AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+            widgetInfo.provider = cn;
+            ReflectionHelpers.setField(widgetInfo, "providerInfo",
+                    packageManager.addReceiverIfNotPresent(cn));
+
+            widgetItems.add(new WidgetItem(
+                    LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
+                    mTestProfile, mIconCache));
+        }
+        return widgetItems;
+    }
+
+    private void assertWidgetCellWithLabel(View view, String label) {
+        assertThat(view).isInstanceOf(WidgetCell.class);
+        TextView widgetLabel = (TextView) view.findViewById(R.id.widget_name);
+        assertThat(widgetLabel.getText()).isEqualTo(label);
+    }
+
+    private final class FakeOnHeaderClickListener implements OnHeaderClickListener {
+
+        boolean mShowWidgets = false;
+        @Nullable  String mHeaderClickedPackage = null;
+
+        @Override
+        public void onHeaderClicked(boolean showWidgets, String packageName) {
+            mShowWidgets = showWidgets;
+            mHeaderClickedPackage = packageName;
+        }
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
new file mode 100644
index 0000000..8a0cf34
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import static android.os.Looper.getMainLooper;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+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 android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.FrameLayout;
+import android.widget.TableRow;
+import android.widget.TextView;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.WidgetPreviewLoader;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.android.controller.ActivityController;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsListTableViewHolderBinderTest {
+    private static final String TEST_PACKAGE = "com.google.test";
+    private static final String APP_NAME = "Test app";
+
+    private Context mContext;
+    private WidgetsListTableViewHolderBinder mViewHolderBinder;
+    private InvariantDeviceProfile mTestProfile;
+    // Replace ActivityController with ActivityScenario, which is the recommended way for activity
+    // testing.
+    private ActivityController<TestActivity> mActivityController;
+    private TestActivity mTestActivity;
+
+    @Mock
+    private OnLongClickListener mOnLongClickListener;
+    @Mock
+    private OnClickListener mOnIconClickListener;
+    @Mock
+    private IconCache mIconCache;
+    @Mock
+    private WidgetPreviewLoader mWidgetPreviewLoader;
+    @Mock
+    private DeviceProfile mDeviceProfile;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mTestProfile = new InvariantDeviceProfile();
+        mTestProfile.numRows = 5;
+        mTestProfile.numColumns = 5;
+
+        mActivityController = Robolectric.buildActivity(TestActivity.class);
+        mTestActivity = mActivityController.setup().get();
+        mTestActivity.setDeviceProfile(mDeviceProfile);
+
+        doAnswer(invocation -> {
+            ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+            return componentWithLabel.getComponent().getShortClassName();
+        }).when(mIconCache).getTitleNoCache(any());
+
+        mViewHolderBinder = new WidgetsListTableViewHolderBinder(
+                mContext,
+                LayoutInflater.from(mTestActivity),
+                mOnIconClickListener,
+                mOnLongClickListener,
+                mWidgetPreviewLoader);
+    }
+
+    @After
+    public void tearDown() {
+        mActivityController.destroy();
+    }
+
+    @Test
+    public void bindViewHolder_appWith3Widgets_shouldHave3Widgets() {
+        WidgetsRowViewHolder viewHolder = mViewHolderBinder.newViewHolder(
+                new FrameLayout(mTestActivity));
+        WidgetsListContentEntry entry = generateSampleAppWithWidgets(
+                APP_NAME,
+                TEST_PACKAGE,
+                /* numOfWidgets= */ 3);
+        mViewHolderBinder.bindViewHolder(viewHolder, entry);
+        shadowOf(getMainLooper()).idle();
+
+        // THEN the table container has one row, which contains 3 widgets.
+        // View:  .SampleWidget0 | .SampleWidget1 | .SampleWidget2
+        assertThat(viewHolder.mTableContainer.getChildCount()).isEqualTo(1);
+        TableRow row = (TableRow) viewHolder.mTableContainer.getChildAt(0);
+        assertThat(row.getChildCount()).isEqualTo(3);
+        // Widget 0 label is .SampleWidget0.
+        assertWidgetCellWithLabel(row.getChildAt(0), ".SampleWidget0");
+        // Widget 1 label is .SampleWidget1.
+        assertWidgetCellWithLabel(row.getChildAt(1), ".SampleWidget1");
+        // Widget 2 label is .SampleWidget2.
+        assertWidgetCellWithLabel(row.getChildAt(2), ".SampleWidget2");
+    }
+
+    private WidgetsListContentEntry generateSampleAppWithWidgets(String appName, String packageName,
+            int numOfWidgets) {
+        PackageItemInfo appInfo = new PackageItemInfo(packageName);
+        appInfo.title = appName;
+        appInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);
+
+        return new WidgetsListContentEntry(appInfo,
+                /* titleSectionName= */ "",
+                generateWidgetItems(packageName, numOfWidgets));
+    }
+
+    private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) {
+        ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+        ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+        for (int i = 0; i < numOfWidgets; i++) {
+            ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i);
+            AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+            widgetInfo.provider = cn;
+            ReflectionHelpers.setField(widgetInfo, "providerInfo",
+                    packageManager.addReceiverIfNotPresent(cn));
+
+            widgetItems.add(new WidgetItem(
+                    LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
+                    mTestProfile, mIconCache));
+        }
+        return widgetItems;
+    }
+
+    private void assertWidgetCellWithLabel(View view, String label) {
+        assertThat(view).isInstanceOf(WidgetCell.class);
+        TextView widgetLabel = (TextView) view.findViewById(R.id.widget_name);
+        assertThat(widgetLabel.getText()).isEqualTo(label);
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
new file mode 100644
index 0000000..2d22c45
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2021 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.widget.picker.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsListContentEntryTest {
+    private static final String PACKAGE_NAME = "com.google.test";
+    private final PackageItemInfo mPackageItemInfo = new PackageItemInfo(PACKAGE_NAME);
+    private final ComponentName mWidget1 = ComponentName.createRelative(PACKAGE_NAME, ".mWidget1");
+    private final ComponentName mWidget2 = ComponentName.createRelative(PACKAGE_NAME, ".mWidget2");
+    private final ComponentName mWidget3 = ComponentName.createRelative(PACKAGE_NAME, ".mWidget3");
+    private final Map<ComponentName, String> mWidgetsToLabels = new HashMap();
+
+    @Mock private IconCache mIconCache;
+
+    private Context mContext;
+    private InvariantDeviceProfile mTestProfile;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mWidgetsToLabels.put(mWidget1, "Cat");
+        mWidgetsToLabels.put(mWidget2, "Dog");
+        mWidgetsToLabels.put(mWidget3, "Bird");
+
+        mContext = RuntimeEnvironment.application;
+        mTestProfile = new InvariantDeviceProfile();
+        mTestProfile.numRows = 5;
+        mTestProfile.numColumns = 5;
+
+        doAnswer(invocation -> {
+            ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0);
+            return mWidgetsToLabels.get(componentWithLabel.getComponent());
+        }).when(mIconCache).getTitleNoCache(any());
+    }
+
+    @Test
+    public void unsortedWidgets_diffLabels_shouldSortWidgetItems() {
+        // GIVEN a list of widgets in unsorted order.
+        // Cat 2x3
+        WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 2, /* spanY= */ 3);
+        // Dog 2x3
+        WidgetItem widgetItem2 = createWidgetItem(mWidget2, /* spanX= */ 2, /* spanY= */ 3);
+        // Bird 2x3
+        WidgetItem widgetItem3 = createWidgetItem(mWidget3, /* spanX= */ 2, /* spanY= */ 3);
+
+        // WHEN creates a WidgetsListRowEntry with the unsorted widgets.
+        WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(mPackageItemInfo,
+                /* titleSectionName= */ "T",
+                List.of(widgetItem1, widgetItem2, widgetItem3));
+
+        // THEN the widgets list is sorted by their labels alphabetically: [Bird, Cat, Dog].
+        assertThat(widgetsListRowEntry.mWidgets)
+                .containsExactly(widgetItem3, widgetItem1, widgetItem2)
+                .inOrder();
+        assertThat(widgetsListRowEntry.mTitleSectionName).isEqualTo("T");
+        assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(mPackageItemInfo);
+    }
+
+    @Test
+    public void unsortedWidgets_sameLabels_differentSize_shouldSortWidgetItems() {
+        // GIVEN a list of widgets in unsorted order.
+        // Cat 3x3
+        WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 3, /* spanY= */ 3);
+        // Cat 1x2
+        WidgetItem widgetItem2 = createWidgetItem(mWidget1, /* spanX= */ 1, /* spanY= */ 2);
+        // Cat 2x2
+        WidgetItem widgetItem3 = createWidgetItem(mWidget1, /* spanX= */ 2, /* spanY= */ 2);
+
+        // WHEN creates a WidgetsListRowEntry with the unsorted widgets.
+        WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(mPackageItemInfo,
+                /* titleSectionName= */ "T",
+                List.of(widgetItem1, widgetItem2, widgetItem3));
+
+        // THEN the widgets list is sorted by their gird sizes in an ascending order:
+        // [1x2, 2x2, 3x3].
+        assertThat(widgetsListRowEntry.mWidgets)
+                .containsExactly(widgetItem2, widgetItem3, widgetItem1)
+                .inOrder();
+        assertThat(widgetsListRowEntry.mTitleSectionName).isEqualTo("T");
+        assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(mPackageItemInfo);
+    }
+
+    @Test
+    public void unsortedWidgets_hodgepodge_shouldSortWidgetItems() {
+        // GIVEN a list of widgets in unsorted order.
+        // Cat 3x3
+        WidgetItem widgetItem1 = createWidgetItem(mWidget1, /* spanX= */ 3, /* spanY= */ 3);
+        // Cat 1x2
+        WidgetItem widgetItem2 = createWidgetItem(mWidget1, /* spanX= */ 1, /* spanY= */ 2);
+        // Dog 2x2
+        WidgetItem widgetItem3 = createWidgetItem(mWidget2, /* spanX= */ 2, /* spanY= */ 2);
+        // Bird 2x2
+        WidgetItem widgetItem4 = createWidgetItem(mWidget3, /* spanX= */ 2, /* spanY= */ 2);
+
+        // WHEN creates a WidgetsListRowEntry with the unsorted widgets.
+        WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(mPackageItemInfo,
+                /* titleSectionName= */ "T",
+                List.of(widgetItem1, widgetItem2, widgetItem3, widgetItem4));
+
+        // THEN the widgets list is first sorted by labels alphabetically. Then, for widgets with
+        // same labels, they are sorted by their gird sizes in an ascending order:
+        // [Bird 2x2, Cat 1x2, Cat 3x3, Dog 2x2]
+        assertThat(widgetsListRowEntry.mWidgets)
+                .containsExactly(widgetItem4, widgetItem2, widgetItem1, widgetItem3)
+                .inOrder();
+        assertThat(widgetsListRowEntry.mTitleSectionName).isEqualTo("T");
+        assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(mPackageItemInfo);
+    }
+
+    private WidgetItem createWidgetItem(ComponentName componentName, int spanX, int spanY) {
+        String label = mWidgetsToLabels.get(componentName);
+        ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+        AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo();
+        widgetInfo.provider = componentName;
+        ReflectionHelpers.setField(widgetInfo, "providerInfo",
+                packageManager.addReceiverIfNotPresent(componentName));
+
+        LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo =
+                LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo);
+        launcherAppWidgetProviderInfo.spanX = spanX;
+        launcherAppWidgetProviderInfo.spanY = spanY;
+        launcherAppWidgetProviderInfo.label = label;
+
+        return new WidgetItem(launcherAppWidgetProviderInfo, mTestProfile, mIconCache);
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
new file mode 100644
index 0000000..e68edd3
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2021 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.widget.picker.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.Point;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.icons.ComponentWithLabel;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.pm.ShortcutConfigActivityInfo;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.widget.util.WidgetsTableUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class WidgetsTableUtilsTest {
+    private static final String TEST_PACKAGE = "com.google.test";
+
+    @Mock
+    private IconCache mIconCache;
+
+    private Context mContext;
+    private InvariantDeviceProfile mTestProfile;
+    private WidgetItem mWidget1x1;
+    private WidgetItem mWidget2x2;
+    private WidgetItem mWidget2x3;
+    private WidgetItem mWidget2x4;
+    private WidgetItem mWidget4x4;
+
+    private WidgetItem mShortcut1;
+    private WidgetItem mShortcut2;
+    private WidgetItem mShortcut3;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = RuntimeEnvironment.application;
+
+        mTestProfile = new InvariantDeviceProfile();
+        mTestProfile.numRows = 5;
+        mTestProfile.numColumns = 5;
+
+        initTestWidgets();
+        initTestShortcuts();
+
+        doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0))
+                .getComponent().getPackageName())
+                .when(mIconCache).getTitleNoCache(any());
+    }
+
+
+    @Test
+    public void groupWidgetItemsIntoTable_widgetsOnly_maxSpansPerRow5_shouldGroupWidgetsInTable() {
+        List<WidgetItem> widgetItems = List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4,
+                mWidget2x2);
+
+        List<ArrayList<WidgetItem>> widgetItemInTable = WidgetsTableUtils.groupWidgetItemsIntoTable(
+                widgetItems, /* maxSpansPerRow= */ 5);
+
+        // Row 0: 1x1, 2x2, 2x3
+        // Row 1: 2x4
+        // Row 2: 4x4
+        assertThat(widgetItemInTable).hasSize(3);
+        assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2, mWidget2x3);
+        assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x4);
+        assertThat(widgetItemInTable.get(2)).containsExactly(mWidget4x4);
+    }
+
+    @Test
+    public void groupWidgetItemsIntoTable_widgetsOnly_maxSpansPerRow4_shouldGroupWidgetsInTable() {
+        List<WidgetItem> widgetItems = List.of(mWidget4x4, mWidget2x3, mWidget1x1, mWidget2x4,
+                mWidget2x2);
+
+        List<ArrayList<WidgetItem>> widgetItemInTable = WidgetsTableUtils.groupWidgetItemsIntoTable(
+                widgetItems, /* maxSpansPerRow= */ 4);
+
+        // Row 0: 1x1, 2x2
+        // Row 1: 2x3, 2x4
+        // Row 2: 4x4
+        assertThat(widgetItemInTable).hasSize(3);
+        assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2);
+        assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x3, mWidget2x4);
+        assertThat(widgetItemInTable.get(2)).containsExactly(mWidget4x4);
+    }
+
+    @Test
+    public void groupWidgetItemsIntoTable_mixItems_maxSpansPerRow4_shouldGroupWidgetsInTable() {
+        List<WidgetItem> widgetItems = List.of(mWidget4x4, mShortcut3, mWidget2x3, mShortcut1,
+                mWidget1x1, mShortcut2, mWidget2x4, mWidget2x2);
+
+        List<ArrayList<WidgetItem>> widgetItemInTable = WidgetsTableUtils.groupWidgetItemsIntoTable(
+                widgetItems, /* maxSpansPerRow= */ 4);
+
+        // Row 0: 1x1, 2x2
+        // Row 1: 2x3, 2x4
+        // Row 2: 4x4
+        // Row 3: shortcut3, shortcut1, shortcut2
+        assertThat(widgetItemInTable).hasSize(4);
+        assertThat(widgetItemInTable.get(0)).containsExactly(mWidget1x1, mWidget2x2);
+        assertThat(widgetItemInTable.get(1)).containsExactly(mWidget2x3, mWidget2x4);
+        assertThat(widgetItemInTable.get(2)).containsExactly(mWidget4x4);
+        assertThat(widgetItemInTable.get(3)).containsExactly(mShortcut3, mShortcut2, mShortcut1);
+    }
+
+    private void initTestWidgets() {
+        List<Point> widgetSizes = List.of(new Point(1, 1), new Point(2, 2), new Point(2, 3),
+                new Point(2, 4), new Point(4, 4));
+
+        ArrayList<WidgetItem> widgetItems = new ArrayList<>();
+        widgetSizes.stream().forEach(
+                widgetSize -> {
+                    ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
+                    AppWidgetProviderInfo info = new AppWidgetProviderInfo();
+                    info.provider = ComponentName.createRelative(TEST_PACKAGE,
+                            ".WidgetProvider_" + widgetSize.x + "x" + widgetSize.y);
+                    LauncherAppWidgetProviderInfo widgetInfo =
+                            LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info);
+                    widgetInfo.spanX = widgetSize.x;
+                    widgetInfo.spanY = widgetSize.y;
+                    ReflectionHelpers.setField(widgetInfo, "providerInfo",
+                            packageManager.addReceiverIfNotPresent(widgetInfo.provider));
+                    widgetItems.add(new WidgetItem(widgetInfo, mTestProfile, mIconCache));
+                }
+        );
+        mWidget1x1 = widgetItems.get(0);
+        mWidget2x2 = widgetItems.get(1);
+        mWidget2x3 = widgetItems.get(2);
+        mWidget2x4 = widgetItems.get(3);
+        mWidget4x4 = widgetItems.get(4);
+    }
+
+    private void initTestShortcuts() {
+        PackageManager packageManager = mContext.getPackageManager();
+        mShortcut1 = new WidgetItem(new TestShortcutConfigActivityInfo(
+                ComponentName.createRelative(TEST_PACKAGE, ".shortcut1"), UserHandle.CURRENT),
+                mIconCache, packageManager);
+        mShortcut2 = new WidgetItem(new TestShortcutConfigActivityInfo(
+                ComponentName.createRelative(TEST_PACKAGE, ".shortcut2"), UserHandle.CURRENT),
+                mIconCache, packageManager);
+        mShortcut3 = new WidgetItem(new TestShortcutConfigActivityInfo(
+                ComponentName.createRelative(TEST_PACKAGE, ".shortcut3"), UserHandle.CURRENT),
+                mIconCache, packageManager);
+
+    }
+
+    private final class TestShortcutConfigActivityInfo extends ShortcutConfigActivityInfo {
+
+        TestShortcutConfigActivityInfo(ComponentName componentName, UserHandle user) {
+            super(componentName, user);
+        }
+
+        @Override
+        public Drawable getFullResIcon(IconCache cache) {
+            return null;
+        }
+
+        @Override
+        public CharSequence getLabel(PackageManager pm) {
+            return null;
+        }
+    }
+}
diff --git a/robolectric_tests/unstaged/SettingsCacheTest.java b/robolectric_tests/unstaged/SettingsCacheTest.java
new file mode 100644
index 0000000..fbf4c63
--- /dev/null
+++ b/robolectric_tests/unstaged/SettingsCacheTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2021 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.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.net.Uri;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Collections;
+
+@RunWith(RobolectricTestRunner.class)
+public class SettingsCacheTest {
+
+    public static final Uri KEY_SYSTEM_URI_TEST1 = Uri.parse("content://settings/system/test1");
+    public static final Uri KEY_SYSTEM_URI_TEST2 = Uri.parse("content://settings/system/test2");;
+
+    private SettingsCache.OnChangeListener mChangeListener;
+    private SettingsCache mSettingsCache;
+
+    @Before
+    public void setup() {
+        mChangeListener = mock(SettingsCache.OnChangeListener.class);
+        Context targetContext = RuntimeEnvironment.application;
+        mSettingsCache = SettingsCache.INSTANCE.get(targetContext);
+        mSettingsCache.register(KEY_SYSTEM_URI_TEST1, mChangeListener);
+    }
+
+    @Test
+    public void listenerCalledOnChange() {
+        mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1);
+        verify(mChangeListener, times(1)).onSettingsChanged(true);
+    }
+
+    @Test
+    public void getValueRespectsDefaultValue() {
+        // Case of key not found
+        boolean val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0);
+        assertFalse(val);
+    }
+
+    @Test
+    public void getValueHitsCache() {
+        mSettingsCache.setKeyCache(Collections.singletonMap(KEY_SYSTEM_URI_TEST1, true));
+        boolean val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0);
+        assertTrue(val);
+    }
+
+    @Test
+    public void getValueUpdatedCache() {
+        // First ensure there's nothing in cache
+        boolean val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0);
+        assertFalse(val);
+
+        mSettingsCache.setKeyCache(Collections.singletonMap(KEY_SYSTEM_URI_TEST1, true));
+        val = mSettingsCache.getValue(KEY_SYSTEM_URI_TEST1, 0);
+        assertTrue(val);
+    }
+
+    @Test
+    public void multipleListenersSingleKey() {
+        SettingsCache.OnChangeListener secondListener = mock(SettingsCache.OnChangeListener.class);
+        mSettingsCache.register(KEY_SYSTEM_URI_TEST1, secondListener);
+
+        mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1);
+        verify(mChangeListener, times(1)).onSettingsChanged(true);
+        verify(secondListener, times(1)).onSettingsChanged(true);
+    }
+
+    @Test
+    public void singleListenerMultipleKeys() {
+        SettingsCache.OnChangeListener secondListener = mock(SettingsCache.OnChangeListener.class);
+        mSettingsCache.register(KEY_SYSTEM_URI_TEST2, secondListener);
+
+        mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1);
+        mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST2);
+        verify(mChangeListener, times(1)).onSettingsChanged(true);
+        verify(secondListener, times(1)).onSettingsChanged(true);
+    }
+
+    @Test
+    public void sameListenerMultipleKeys() {
+        SettingsCache.OnChangeListener secondListener = mock(SettingsCache.OnChangeListener.class);
+        mSettingsCache.register(KEY_SYSTEM_URI_TEST2, mChangeListener);
+
+        mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST1);
+        mSettingsCache.onChange(true, KEY_SYSTEM_URI_TEST2);
+        verify(mChangeListener, times(2)).onSettingsChanged(true);
+        verify(secondListener, times(0)).onSettingsChanged(true);
+    }
+}
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 3c34444..95cdbdd 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -59,7 +59,7 @@
             TYPE_SNACKBAR,
             TYPE_LISTENER,
             TYPE_ALL_APPS_EDU,
-
+            TYPE_DRAG_DROP_POPUP,
             TYPE_TASK_MENU,
             TYPE_OPTIONS_POPUP,
             TYPE_ICON_SURFACE
@@ -76,17 +76,18 @@
     public static final int TYPE_SNACKBAR = 1 << 7;
     public static final int TYPE_LISTENER = 1 << 8;
     public static final int TYPE_ALL_APPS_EDU = 1 << 9;
+    public static final int TYPE_DRAG_DROP_POPUP = 1 << 10;
 
     // Popups related to quickstep UI
-    public static final int TYPE_TASK_MENU = 1 << 10;
-    public static final int TYPE_OPTIONS_POPUP = 1 << 11;
-    public static final int TYPE_ICON_SURFACE = 1 << 12;
+    public static final int TYPE_TASK_MENU = 1 << 11;
+    public static final int TYPE_OPTIONS_POPUP = 1 << 12;
+    public static final int TYPE_ICON_SURFACE = 1 << 13;
 
     public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP
             | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET
             | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU
             | TYPE_OPTIONS_POPUP | TYPE_SNACKBAR | TYPE_LISTENER | TYPE_ALL_APPS_EDU
-            | TYPE_ICON_SURFACE;
+            | TYPE_ICON_SURFACE | TYPE_DRAG_DROP_POPUP;
 
     // Type of popups which should be kept open during launcher rebind
     public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET
@@ -103,7 +104,7 @@
     // These view all have particular operation associated with swipe down interaction.
     public static final int TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW = TYPE_WIDGETS_BOTTOM_SHEET |
             TYPE_WIDGETS_FULL_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_ON_BOARD_POPUP |
-            TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU ;
+            TYPE_DISCOVERY_BOUNCE | TYPE_TASK_MENU | TYPE_DRAG_DROP_POPUP;
 
     protected boolean mIsOpen;
 
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index 8df5412..9d6af9f 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -2,6 +2,7 @@
 
 import static com.android.launcher3.LauncherAnimUtils.LAYOUT_HEIGHT;
 import static com.android.launcher3.LauncherAnimUtils.LAYOUT_WIDTH;
+import static com.android.launcher3.Utilities.ATLEAST_S;
 import static com.android.launcher3.views.BaseDragLayer.LAYOUT_X;
 import static com.android.launcher3.views.BaseDragLayer.LAYOUT_Y;
 
@@ -13,16 +14,21 @@
 import android.content.Context;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.util.AttributeSet;
+import android.util.SizeF;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -42,6 +48,14 @@
                         inv.portraitProfile.getCellSize()};
             });
 
+    // Represents the border spacing size on the grid in the two orientations.
+    public static final MainThreadInitializedObject<int[]> BORDER_SPACING_SIZE =
+            new MainThreadInitializedObject<>(c -> {
+                InvariantDeviceProfile inv = LauncherAppState.getIDP(c);
+                return new int[] {inv.landscapeProfile.cellLayoutBorderSpacingPx,
+                        inv.portraitProfile.cellLayoutBorderSpacingPx};
+            });
+
     private static final int HANDLE_COUNT = 4;
     private static final int INDEX_LEFT = 0;
     private static final int INDEX_TOP = 1;
@@ -278,8 +292,9 @@
      *  Based on the current deltas, we determine if and how to resize the widget.
      */
     private void resizeWidgetIfNeeded(boolean onDismiss) {
-        float xThreshold = mCellLayout.getCellWidth();
-        float yThreshold = mCellLayout.getCellHeight();
+        DeviceProfile dp = mLauncher.getDeviceProfile();
+        float xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacingPx;
+        float yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacingPx;
 
         int hSpanInc = getSpanIncrement((mDeltaX + mDeltaXAddOn) / xThreshold - mRunningHInc);
         int vSpanInc = getSpanIncrement((mDeltaY + mDeltaYAddOn) / yThreshold - mRunningVInc);
@@ -350,28 +365,80 @@
     }
 
     public static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher,
-            int spanX, int spanY) {
-        getWidgetSizeRanges(launcher, spanX, spanY, sTmpRect);
-        widgetView.updateAppWidgetSize(null, sTmpRect.left, sTmpRect.top,
-                sTmpRect.right, sTmpRect.bottom);
+                                              int spanX, int spanY) {
+        List<SizeF> sizes = getWidgetSizes(launcher, spanX, spanY);
+        if (ATLEAST_S) {
+            widgetView.updateAppWidgetSize(new Bundle(), sizes);
+        } else {
+            Rect bounds = getMinMaxSizes(sizes, null /* outRect */);
+            widgetView.updateAppWidgetSize(new Bundle(), bounds.left, bounds.top, bounds.right,
+                    bounds.bottom);
+        }
     }
 
-    public static Rect getWidgetSizeRanges(Context context, int spanX, int spanY, Rect rect) {
-        if (rect == null) {
-            rect = new Rect();
-        }
+    private static SizeF getWidgetSize(Context context, Point cellSize, int spanX, int spanY,
+            int borderSpacing) {
         final float density = context.getResources().getDisplayMetrics().density;
+        final float hBorderSpacing = (spanX - 1) * borderSpacing;
+        final float vBorderSpacing = (spanY - 1) * borderSpacing;
+
+        return new SizeF(((spanX * cellSize.x) + hBorderSpacing) / density,
+                ((spanY * cellSize.y) + vBorderSpacing) / density);
+    }
+
+    /** Returns the list of sizes for a widget of given span, in dp. */
+    public static ArrayList<SizeF> getWidgetSizes(Context context, int spanX, int spanY) {
         final Point[] cellSize = CELL_SIZE.get(context);
+        final int[] borderSpacing = BORDER_SPACING_SIZE.get(context);
 
-        // Compute landscape size
-        int landWidth = (int) ((spanX * cellSize[0].x) / density);
-        int landHeight = (int) ((spanY * cellSize[0].y) / density);
+        SizeF landSize = getWidgetSize(context, cellSize[0], spanX, spanY, borderSpacing[0]);
+        SizeF portSize = getWidgetSize(context, cellSize[1], spanX, spanY, borderSpacing[1]);
 
-        // Compute portrait size
-        int portWidth = (int) ((spanX * cellSize[1].x) / density);
-        int portHeight = (int) ((spanY * cellSize[1].y) / density);
-        rect.set(portWidth, landHeight, landWidth, portHeight);
-        return rect;
+        ArrayList<SizeF> sizes = new ArrayList<>(2);
+        sizes.add(landSize);
+        sizes.add(portSize);
+        return sizes;
+    }
+
+    /**
+     * Returns the min and max widths and heights given a list of sizes, in dp.
+     *
+     * @param sizes List of sizes to get the min/max from.
+     * @param outRect Rectangle in which the result can be stored, to avoid extra allocations. If
+     *               null, a new rectangle will be allocated.
+     * @return A rectangle with the left (resp. top) is used for the min width (resp. height) and
+     * the right (resp. bottom) for the max. The returned rectangle is set with 0s if the list is
+     * empty.
+     */
+    public static Rect getMinMaxSizes(List<SizeF> sizes, @Nullable Rect outRect) {
+        if (outRect == null) {
+            outRect = new Rect();
+        }
+        if (sizes.isEmpty()) {
+            outRect.set(0, 0, 0, 0);
+        } else {
+            SizeF first = sizes.get(0);
+            outRect.set((int) first.getWidth(), (int) first.getHeight(), (int) first.getWidth(),
+                    (int) first.getHeight());
+            for (int i = 1; i < sizes.size(); i++) {
+                outRect.union((int) sizes.get(i).getWidth(), (int) sizes.get(i).getHeight());
+            }
+        }
+        return outRect;
+    }
+
+    /**
+     * Returns the range of sizes a widget may be displayed, given its span.
+     *
+     * @param context Context in which the View is rendered.
+     * @param spanX Width of the widget, in cells.
+     * @param spanY Height of the widget, in cells.
+     * @param outRect Rectangle in which the result can be stored, to avoid extra allocations. If
+     *               null, a new rectangle will be allocated.
+     */
+    public static Rect getWidgetSizeRanges(Context context, int spanX, int spanY,
+            @Nullable Rect outRect) {
+        return getMinMaxSizes(getWidgetSizes(context, spanX, spanY), outRect);
     }
 
     @Override
@@ -383,8 +450,9 @@
     }
 
     private void onTouchUp() {
-        int xThreshold = mCellLayout.getCellWidth();
-        int yThreshold = mCellLayout.getCellHeight();
+        DeviceProfile dp = mLauncher.getDeviceProfile();
+        int xThreshold = mCellLayout.getCellWidth() + dp.cellLayoutBorderSpacingPx;
+        int yThreshold = mCellLayout.getCellHeight() + dp.cellLayoutBorderSpacingPx;
 
         mDeltaXAddOn = mRunningHInc * xThreshold;
         mDeltaYAddOn = mRunningVInc * yThreshold;
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index b85c648..75e89b2 100644
--- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
+++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
@@ -21,6 +21,7 @@
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.util.ContentWriter;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
 
 public class AppWidgetsRestoredReceiver extends BroadcastReceiver {
 
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index 5e50e27..062ab71 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -31,7 +31,6 @@
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.util.Log;
-import android.view.ContextThemeWrapper;
 
 import androidx.annotation.IntDef;
 
@@ -141,7 +140,10 @@
         return mDeviceProfile;
     }
 
-    public final StatsLogManager getStatsLogManager() {
+    /**
+     * Returns {@link StatsLogManager} for user event logging.
+     */
+    public StatsLogManager getStatsLogManager() {
         if (mStatsLogManager == null) {
             mStatsLogManager = StatsLogManager.newInstance(this);
         }
@@ -330,7 +332,7 @@
     public static <T extends BaseActivity> T fromContext(Context context) {
         if (context instanceof BaseActivity) {
             return (T) context;
-        } else if (context instanceof ContextThemeWrapper) {
+        } else if (context instanceof ContextWrapper) {
             return fromContext(((ContextWrapper) context).getBaseContext());
         } else {
             throw new IllegalArgumentException("Cannot find BaseActivity in parent tree");
diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java
index c55b46b..9369bdc 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -23,6 +23,7 @@
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityNodeInfo;
 
+import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.compat.AccessibilityManagerCompat;
@@ -188,4 +189,21 @@
         super.onInitializeAccessibilityNodeInfo(info);
         if (isLayoutSuppressed()) info.setScrollable(false);
     }
+
+    /**
+     * Scrolls this recycler view to the top.
+     */
+    public void scrollToTop() {
+        if (mScrollbar != null) {
+            mScrollbar.reattachThumbToScroll();
+        }
+        if (getLayoutManager() instanceof LinearLayoutManager) {
+            LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
+            if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
+                // We are at the top, so don't scrollToPosition (would cause unnecessary relayout).
+                return;
+            }
+        }
+        scrollToPosition(0);
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 575d6cd..e0be6de 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -24,7 +24,6 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.TypedArray;
@@ -34,8 +33,6 @@
 import android.graphics.Paint;
 import android.graphics.Path;
 import android.graphics.PointF;
-import android.graphics.PorterDuff.Mode;
-import android.graphics.PorterDuffColorFilter;
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
@@ -51,11 +48,10 @@
 import android.widget.TextView;
 
 import androidx.annotation.Nullable;
-import androidx.core.graphics.ColorUtils;
+import androidx.annotation.UiThread;
 
 import com.android.launcher3.Launcher.OnResumeCallback;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
-import com.android.launcher3.allapps.AllAppsSectionDecorator;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.dragndrop.DraggableView;
@@ -65,9 +61,9 @@
 import com.android.launcher3.graphics.PlaceHolderIconDrawable;
 import com.android.launcher3.graphics.PreloadIconDrawable;
 import com.android.launcher3.icons.DotRenderer;
-import com.android.launcher3.icons.IconCache.IconLoadRequest;
 import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.icons.LauncherIcons;
+import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -86,12 +82,13 @@
  * too aggressive.
  */
 public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, OnResumeCallback,
-        IconLabelDotView, DraggableView, Reorderable, AllAppsSectionDecorator.SelfDecoratingView {
+        IconLabelDotView, DraggableView, Reorderable {
 
     private static final int DISPLAY_WORKSPACE = 0;
     private static final int DISPLAY_ALL_APPS = 1;
     private static final int DISPLAY_FOLDER = 2;
     private static final int DISPLAY_HERO_APP = 5;
+    protected static final int DISPLAY_TASKBAR = 6;
 
     private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed};
     private static final float HIGHLIGHT_SCALE = 1.16f;
@@ -140,7 +137,7 @@
     private Drawable mIcon;
     private boolean mCenterVertically;
 
-    private final int mDisplay;
+    protected final int mDisplay;
 
     private final CheckLongPressHelper mLongPressHelper;
 
@@ -169,7 +166,7 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mDisableRelayout = false;
 
-    private IconLoadRequest mIconLoadRequest;
+    private HandlerRunnable mIconLoadRequest;
 
     private boolean mEnableIconUpdateAnimation = false;
 
@@ -196,6 +193,7 @@
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx);
             setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
             defaultIconSize = grid.iconSizePx;
+            setCenterVertically(grid.isScalableGrid);
         } else if (mDisplay == DISPLAY_ALL_APPS) {
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx);
             setCompoundDrawablePadding(grid.allAppsIconDrawablePaddingPx);
@@ -206,6 +204,8 @@
             defaultIconSize = grid.folderChildIconSizePx;
         } else if (mDisplay == DISPLAY_HERO_APP) {
             defaultIconSize = grid.allAppsIconSizePx;
+        } else if (mDisplay == DISPLAY_TASKBAR) {
+            defaultIconSize = grid.iconSizePx;
         } else {
             // widget_selection or shortcut_popup
             defaultIconSize = grid.iconSizePx;
@@ -228,7 +228,6 @@
         int shadowSize = context.getResources().getDimensionPixelSize(
                 R.dimen.blur_size_click_shadow);
         mHighlightShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.INNER);
-
     }
 
     @Override
@@ -268,6 +267,7 @@
         mDotScaleAnim.start();
     }
 
+    @UiThread
     public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
         applyFromWorkspaceItem(info, false);
     }
@@ -284,13 +284,16 @@
         }
     }
 
+    @UiThread
     public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean promiseStateChanged) {
         applyIconAndLabel(info);
         setTag(info);
         applyLoadingState(promiseStateChanged);
         applyDotState(info, false /* animate */);
+        setDownloadStateContentDescription(info, info.getProgressLevel());
     }
 
+    @UiThread
     public void applyFromApplicationInfo(AppInfo info) {
         applyIconAndLabel(info);
 
@@ -304,11 +307,13 @@
             applyProgressLevel();
         }
         applyDotState(info, false /* animate */);
+        setDownloadStateContentDescription(info, info.getProgressLevel());
     }
 
     /**
      * Apply label and tag using a generic {@link ItemInfoWithIcon}
      */
+    @UiThread
     public void applyFromItemInfoWithIcon(ItemInfoWithIcon info) {
         applyIconAndLabel(info);
         // We don't need to check the info since it's not a WorkspaceItemInfo
@@ -316,16 +321,20 @@
 
         // Verify high res immediately
         verifyHighRes();
+
+        setDownloadStateContentDescription(info, info.getProgressLevel());
     }
 
     /**
      * Apply label and tag using a {@link SearchActionItemInfo}
      */
+    @UiThread
     public void applyFromSearchActionItemInfo(SearchActionItemInfo searchActionItemInfo) {
         applyIconAndLabel(searchActionItemInfo);
         setTag(searchActionItemInfo);
     }
 
+    @UiThread
     protected void applyIconAndLabel(ItemInfoWithIcon info) {
         FastBitmapDrawable iconDrawable = newIcon(getContext(), info);
         mDotParams.color = IconPalette.getMutedColor(info.bitmap.color, 0.54f);
@@ -334,6 +343,7 @@
         applyLabel(info);
     }
 
+    @UiThread
     private void applyLabel(ItemInfoWithIcon info) {
         setText(info.title);
         if (info.contentDescription != null) {
@@ -344,6 +354,16 @@
     }
 
     /**
+     * Directly set the icon and label.
+     */
+    @UiThread
+    public void applyIconAndLabel(Drawable icon, CharSequence label) {
+        setIcon(icon);
+        setText(label);
+        setContentDescription(label);
+    }
+
+    /**
      * Overrides the default long press timeout.
      */
     public void setLongPressTimeoutFactor(float longPressTimeoutFactor) {
@@ -483,6 +503,10 @@
      * @param canvas The canvas to draw to.
      */
     protected void drawDotIfNecessary(Canvas canvas) {
+        if (mActivity instanceof Launcher && ((Launcher) mActivity).isViewInTaskbar(this)) {
+            // TODO: support notification dots in Taskbar
+            return;
+        }
         if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) {
             getIconBounds(mDotParams.iconBounds);
             Utilities.scaleRectAboutCenter(mDotParams.iconBounds,
@@ -525,6 +549,14 @@
         outBounds.set(left, top, right, bottom);
     }
 
+
+    /**
+     * Sets whether to vertically center the content.
+     */
+    public void setCenterVertically(boolean centerVertically) {
+        mCenterVertically = centerVertically;
+    }
+
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         if (mCenterVertically) {
@@ -635,9 +667,7 @@
             setContentDescription(info.contentDescription != null
                     ? info.contentDescription : "");
         } else if (progressLevel > 0) {
-            setContentDescription(getContext()
-                    .getString(R.string.app_downloading_title, info.title,
-                            NumberFormat.getPercentInstance().format(progressLevel * 0.01)));
+            setDownloadStateContentDescription(info, progressLevel);
         } else {
             setContentDescription(getContext()
                     .getString(R.string.app_waiting_download_title, info.title));
@@ -713,6 +743,24 @@
         }
     }
 
+    private void setDownloadStateContentDescription(ItemInfoWithIcon info, int progressLevel) {
+        if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK)
+                != 0) {
+            String percentageString = NumberFormat.getPercentInstance()
+                    .format(progressLevel * 0.01);
+            if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
+                setContentDescription(getContext()
+                        .getString(
+                            R.string.app_installing_title, info.title, percentageString));
+            } else if ((info.runtimeStatusFlags
+                    & ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) {
+                setContentDescription(getContext()
+                        .getString(
+                            R.string.app_downloading_title, info.title, percentageString));
+            }
+        }
+    }
+
     /**
      * Sets the icon for this view based on the layout direction.
      */
@@ -753,7 +801,7 @@
         if (mIcon != null
                 && mIcon instanceof PlaceHolderIconDrawable
                 && iconUpdateAnimationEnabled()) {
-            animateIconUpdate((PlaceHolderIconDrawable) mIcon, icon);
+            ((PlaceHolderIconDrawable) mIcon).animateIconUpdate(icon);
         }
 
         mDisableRelayout = false;
@@ -904,38 +952,4 @@
             setCompoundDrawables(null, newIcon, null, null);
         }
     }
-
-    private static void animateIconUpdate(PlaceHolderIconDrawable oldIcon, Drawable newIcon) {
-        int placeholderColor = oldIcon.mPaint.getColor();
-        int originalAlpha = Color.alpha(placeholderColor);
-
-        ValueAnimator iconUpdateAnimation = ValueAnimator.ofInt(originalAlpha, 0);
-        iconUpdateAnimation.setDuration(ICON_UPDATE_ANIMATION_DURATION);
-        iconUpdateAnimation.addUpdateListener(valueAnimator -> {
-            int newAlpha = (int) valueAnimator.getAnimatedValue();
-            int newColor = ColorUtils.setAlphaComponent(placeholderColor, newAlpha);
-
-            newIcon.setColorFilter(new PorterDuffColorFilter(newColor, Mode.SRC_ATOP));
-        });
-        iconUpdateAnimation.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                newIcon.setColorFilter(null);
-            }
-        });
-        iconUpdateAnimation.start();
-    }
-
-
-    @Override
-    public void decorate(int color) {
-        mHighlightColor = color;
-        invalidate();
-    }
-
-    @Override
-    public void removeDecoration() {
-        mHighlightColor = Color.TRANSPARENT;
-        invalidate();
-    }
 }
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index df005e6..459b9a8 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -240,7 +240,7 @@
 
     @Override
     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
-        mActive = supportsDrop(dragObject.dragInfo);
+        mActive = !options.isKeyboardDrag && supportsDrop(dragObject.dragInfo);
         mDrawable.setColorFilter(null);
         if (mCurrentColorAnim != null) {
             mCurrentColorAnim.cancel();
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 2809bd5..b8833cf 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -19,6 +19,7 @@
 import static android.animation.ValueAnimator.areAnimatorsEnabled;
 
 import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_FOUR_COLUMNS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -52,6 +53,7 @@
 import android.view.accessibility.AccessibilityEvent;
 
 import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
 import androidx.core.view.ViewCompat;
 
 import com.android.launcher3.LauncherSettings.Favorites;
@@ -88,6 +90,8 @@
     @Thunk int mCellHeight;
     private int mFixedCellWidth;
     private int mFixedCellHeight;
+    @ViewDebug.ExportedProperty(category = "launcher")
+    private final int mBorderSpacing;
 
     @ViewDebug.ExportedProperty(category = "launcher")
     private int mCountX;
@@ -208,6 +212,7 @@
 
         DeviceProfile grid = mActivity.getDeviceProfile();
 
+        mBorderSpacing = grid.cellLayoutBorderSpacingPx;
         mCellWidth = mCellHeight = -1;
         mFixedCellWidth = mFixedCellHeight = -1;
 
@@ -288,7 +293,8 @@
         }
 
         mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context, mContainerType);
-        mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY);
+        mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
+                mBorderSpacing);
         addView(mShortcutsAndWidgets);
     }
 
@@ -305,6 +311,8 @@
         setImportantForAccessibility(accessibilityFlag);
         getShortcutsAndWidgets().setImportantForAccessibility(accessibilityFlag);
 
+        // ExploreByTouchHelper sets focusability. Clear it when the delegate is cleared.
+        setFocusable(delegate != null);
         // Invalidate the accessibility hierarchy
         if (getParent() != null) {
             getParent().notifySubtreeAccessibilityStateChanged(
@@ -312,6 +320,13 @@
         }
     }
 
+    /**
+     * Returns the currently set accessibility delegate
+     */
+    public DragAndDropAccessibilityDelegate getDragAndDropAccessibilityDelegate() {
+        return mTouchHelper;
+    }
+
     @Override
     public boolean dispatchHoverEvent(MotionEvent event) {
         // Always attempt to dispatch hover events to accessibility first.
@@ -338,7 +353,8 @@
     public void setCellDimensions(int width, int height) {
         mFixedCellWidth = mCellWidth = width;
         mFixedCellHeight = mCellHeight = height;
-        mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY);
+        mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
+                mBorderSpacing);
     }
 
     public void setGridSize(int x, int y) {
@@ -347,7 +363,8 @@
         mOccupied = new GridOccupancy(mCountX, mCountY);
         mTmpOccupied = new GridOccupancy(mCountX, mCountY);
         mTempRectStack.clear();
-        mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY);
+        mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
+                mBorderSpacing);
         requestLayout();
     }
 
@@ -468,8 +485,8 @@
             for (int j = 0; j < mCountY; j++) {
                 canvas.save();
 
-                int transX = i * mCellWidth;
-                int transY = j * mCellHeight;
+                int transX = i * mCellWidth + (i * mBorderSpacing);
+                int transY = j * mCellHeight + (j * mBorderSpacing);
 
                 canvas.translate(getPaddingLeft() + transX, getPaddingTop() + transY);
 
@@ -584,6 +601,9 @@
         if (child instanceof BubbleTextView) {
             BubbleTextView bubbleChild = (BubbleTextView) child;
             bubbleChild.setTextVisibility(mContainerType != HOTSEAT);
+            if (ENABLE_FOUR_COLUMNS.get()) {
+                bubbleChild.setCenterVertically(mContainerType != HOTSEAT);
+            }
         }
 
         child.setScaleX(mChildScale);
@@ -699,11 +719,9 @@
      * @param result Array of 2 ints to hold the x and y coordinate of the point
      */
     void cellToPoint(int cellX, int cellY, int[] result) {
-        final int hStartPadding = getPaddingLeft();
-        final int vStartPadding = getPaddingTop();
-
-        result[0] = hStartPadding + cellX * mCellWidth;
-        result[1] = vStartPadding + cellY * mCellHeight;
+        cellToRect(cellX, cellY, 1, 1, mTempRect);
+        result[0] = mTempRect.left;
+        result[1] = mTempRect.top;
     }
 
     /**
@@ -727,25 +745,9 @@
      * @param result Array of 2 ints to hold the x and y coordinate of the point
      */
     void regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result) {
-        final int hStartPadding = getPaddingLeft();
-        final int vStartPadding = getPaddingTop();
-        result[0] = hStartPadding + cellX * mCellWidth + (spanX * mCellWidth) / 2;
-        result[1] = vStartPadding + cellY * mCellHeight + (spanY * mCellHeight) / 2;
-    }
-
-     /**
-     * Given a cell coordinate and span fills out a corresponding pixel rect
-     *
-     * @param cellX X coordinate of the cell
-     * @param cellY Y coordinate of the cell
-     * @param result Rect in which to write the result
-     */
-     void regionToRect(int cellX, int cellY, int spanX, int spanY, Rect result) {
-        final int hStartPadding = getPaddingLeft();
-        final int vStartPadding = getPaddingTop();
-        final int left = hStartPadding + cellX * mCellWidth;
-        final int top = vStartPadding + cellY * mCellHeight;
-        result.set(left, top, left + (spanX * mCellWidth), top + (spanY * mCellHeight));
+        cellToRect(cellX, cellY, spanX, spanY, mTempRect);
+        result[0] = mTempRect.centerX();
+        result[1] = mTempRect.centerY();
     }
 
     public float getDistanceFromCell(float x, float y, int[] cell) {
@@ -776,12 +778,15 @@
         int childHeightSize = heightSize - (getPaddingTop() + getPaddingBottom());
 
         if (mFixedCellWidth < 0 || mFixedCellHeight < 0) {
-            int cw = DeviceProfile.calculateCellWidth(childWidthSize, mCountX);
-            int ch = DeviceProfile.calculateCellHeight(childHeightSize, mCountY);
+            int cw = DeviceProfile.calculateCellWidth(childWidthSize, mBorderSpacing,
+                    mCountX);
+            int ch = DeviceProfile.calculateCellHeight(childHeightSize, mBorderSpacing,
+                    mCountY);
             if (cw != mCellWidth || ch != mCellHeight) {
                 mCellWidth = cw;
                 mCellHeight = ch;
-                mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY);
+                mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY,
+                        mBorderSpacing);
             }
         }
 
@@ -831,10 +836,11 @@
     /**
      * Returns the amount of space left over after subtracting padding and cells. This space will be
      * very small, a few pixels at most, and is a result of rounding down when calculating the cell
-     * width in {@link DeviceProfile#calculateCellWidth(int, int)}.
+     * width in {@link DeviceProfile#calculateCellWidth(int, int, int)}.
      */
     public int getUnusedHorizontalSpace() {
-        return getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - (mCountX * mCellWidth);
+        return getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - (mCountX * mCellWidth)
+                - ((mCountX - 1) * mBorderSpacing);
     }
 
     public Drawable getScrimBackground() {
@@ -850,8 +856,8 @@
         return mShortcutsAndWidgets;
     }
 
-    public View getChildAt(int x, int y) {
-        return mShortcutsAndWidgets.getChildAt(x, y);
+    public View getChildAt(int cellX, int cellY) {
+        return mShortcutsAndWidgets.getChildAt(cellX, cellY);
     }
 
     public boolean animateChildToPosition(final View child, int cellX, int cellY, int duration,
@@ -982,11 +988,11 @@
             }
 
             // Center horizontaly
-            left += ((mCellWidth * spanX) - dragOutline.getWidth()) / 2;
+            left += (r.width() - dragOutline.getWidth()) / 2;
 
             if (v != null && v.getViewType() == DraggableView.DRAGGABLE_WIDGET) {
                 // Center vertically
-                top += ((mCellHeight * spanY) - dragOutline.getHeight()) / 2;
+                top += (r.height() - dragOutline.getHeight()) / 2;
             } else if (v != null && v.getViewType() == DraggableView.DRAGGABLE_ICON) {
                 int cHeight = getShortcutsAndWidgets().getCellContentHeight();
                 int cellPaddingY = (int) Math.max(0, ((mCellHeight - cHeight) / 2f));
@@ -2146,7 +2152,7 @@
 
         findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination);
         Rect dragRect = new Rect();
-        regionToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect);
+        cellToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect);
         dragRect.offset(dragViewCenterX - dragRect.centerX(), dragViewCenterY - dragRect.centerY());
 
         Rect dropRegionRect = new Rect();
@@ -2156,7 +2162,7 @@
         int dropRegionSpanX = dropRegionRect.width();
         int dropRegionSpanY = dropRegionRect.height();
 
-        regionToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(),
+        cellToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(),
                 dropRegionRect.height(), dropRegionRect);
 
         int deltaX = (dropRegionRect.centerX() - dragViewCenterX) / spanX;
@@ -2514,10 +2520,11 @@
         final int hStartPadding = getPaddingLeft();
         final int vStartPadding = getPaddingTop();
 
-        int width = cellHSpan * cellWidth;
-        int height = cellVSpan * cellHeight;
-        int x = hStartPadding + cellX * cellWidth;
-        int y = vStartPadding + cellY * cellHeight;
+        int x = hStartPadding + (cellX * mBorderSpacing) + (cellX * cellWidth);
+        int y = vStartPadding + (cellY * mBorderSpacing) + (cellY * cellHeight);
+
+        int width = cellHSpan * cellWidth + ((cellHSpan - 1) * mBorderSpacing);
+        int height = cellVSpan * cellHeight + ((cellVSpan - 1) * mBorderSpacing);
 
         resultRect.set(x, y, x + width, y + height);
     }
@@ -2535,11 +2542,13 @@
     }
 
     public int getDesiredWidth() {
-        return getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth);
+        return getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth)
+                + ((mCountX - 1) * mBorderSpacing);
     }
 
     public int getDesiredHeight()  {
-        return getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight);
+        return getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight)
+                + ((mCountY - 1) * mBorderSpacing);
     }
 
     public boolean isOccupied(int x, int y) {
@@ -2654,19 +2663,22 @@
             this.cellVSpan = cellVSpan;
         }
 
-        public void setup(int cellWidth, int cellHeight, boolean invertHorizontally, int colCount) {
-            setup(cellWidth, cellHeight, invertHorizontally, colCount, 1.0f, 1.0f);
+        public void setup(int cellWidth, int cellHeight, boolean invertHorizontally, int colCount,
+                int rowCount, int borderSpacing, @Nullable Rect inset) {
+            setup(cellWidth, cellHeight, invertHorizontally, colCount, rowCount, 1.0f, 1.0f,
+                    borderSpacing, inset);
         }
 
         /**
-         * Use this method, as opposed to {@link #setup(int, int, boolean, int)}, if the view needs
-         * to be scaled.
+         * Use this method, as opposed to {@link #setup(int, int, boolean, int, int, int, Rect)},
+         * if the view needs to be scaled.
          *
          * ie. In multi-window mode, we setup widgets so that they are measured and laid out
          * using their full/invariant device profile sizes.
          */
         public void setup(int cellWidth, int cellHeight, boolean invertHorizontally, int colCount,
-                float cellScaleX, float cellScaleY) {
+                int rowCount, float cellScaleX, float cellScaleY, int borderSpacing,
+                @Nullable Rect inset) {
             if (isLockedToGrid) {
                 final int myCellHSpan = cellHSpan;
                 final int myCellVSpan = cellVSpan;
@@ -2677,17 +2689,30 @@
                     myCellX = colCount - myCellX - cellHSpan;
                 }
 
-                width = (int) (myCellHSpan * cellWidth / cellScaleX - leftMargin - rightMargin);
-                height = (int) (myCellVSpan * cellHeight / cellScaleY - topMargin - bottomMargin);
-                x = (myCellX * cellWidth + leftMargin);
-                y = (myCellY * cellHeight + topMargin);
+                int hBorderSpacing = (myCellHSpan - 1) * borderSpacing;
+                int vBorderSpacing = (myCellVSpan - 1) * borderSpacing;
+
+                float myCellWidth = ((myCellHSpan * cellWidth) + hBorderSpacing) / cellScaleX;
+                float myCellHeight = ((myCellVSpan * cellHeight) + vBorderSpacing) / cellScaleY;
+
+                width = Math.round(myCellWidth) - leftMargin - rightMargin;
+                height = Math.round(myCellHeight) - topMargin - bottomMargin;
+                x = leftMargin + (myCellX * cellWidth) + (myCellX * borderSpacing);
+                y = topMargin + (myCellY * cellHeight) + (myCellY * borderSpacing);
+
+                if (inset != null) {
+                    x -= inset.left;
+                    y -= inset.top;
+                    width += inset.left + inset.right;
+                    height += inset.top + inset.bottom;
+                }
             }
         }
 
         /**
          * Sets the position to the provided point
          */
-        public void setXY(Point point) {
+        public void setCellXY(Point point) {
             cellX = point.x;
             cellY = point.y;
         }
diff --git a/src/com/android/launcher3/DevicePaddings.java b/src/com/android/launcher3/DevicePaddings.java
new file mode 100644
index 0000000..4827f36
--- /dev/null
+++ b/src/com/android/launcher3/DevicePaddings.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Workspace items have a fixed height, so we need a way to distribute any unused workspace height.
+ *
+ * The unused or "extra" height is allocated to three different variable heights:
+ * - The space above the workspace
+ * - The space between the workspace and hotseat
+ * - The espace below the hotseat
+ */
+public class DevicePaddings {
+
+    private static final String DEVICE_PADDING = "device-paddings";
+    private static final String DEVICE_PADDINGS = "device-padding";
+
+    private static final String WORKSPACE_TOP_PADDING = "workspaceTopPadding";
+    private static final String WORKSPACE_BOTTOM_PADDING = "workspaceBottomPadding";
+    private static final String HOTSEAT_BOTTOM_PADDING = "hotseatBottomPadding";
+
+    private static final String TAG = DevicePaddings.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    ArrayList<DevicePadding> mDevicePaddings = new ArrayList<>();
+
+    public DevicePaddings(Context context) {
+        try (XmlResourceParser parser = context.getResources().getXml(R.xml.size_limits)) {
+            final int depth = parser.getDepth();
+            int type;
+            while (((type = parser.next()) != XmlPullParser.END_TAG ||
+                    parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+                if ((type == XmlPullParser.START_TAG) && DEVICE_PADDING.equals(parser.getName())) {
+                    final int displayDepth = parser.getDepth();
+                    while (((type = parser.next()) != XmlPullParser.END_TAG ||
+                            parser.getDepth() > displayDepth)
+                            && type != XmlPullParser.END_DOCUMENT) {
+                        if ((type == XmlPullParser.START_TAG)
+                                && DEVICE_PADDINGS.equals(parser.getName())) {
+                            TypedArray a = context.obtainStyledAttributes(
+                                    Xml.asAttributeSet(parser), R.styleable.DevicePadding);
+                            int maxWidthPx = a.getDimensionPixelSize(
+                                    R.styleable.DevicePadding_maxEmptySpace, 0);
+                            a.recycle();
+
+                            PaddingFormula workspaceTopPadding = null;
+                            PaddingFormula workspaceBottomPadding = null;
+                            PaddingFormula hotseatBottomPadding = null;
+
+                            final int limitDepth = parser.getDepth();
+                            while (((type = parser.next()) != XmlPullParser.END_TAG ||
+                                    parser.getDepth() > limitDepth)
+                                    && type != XmlPullParser.END_DOCUMENT) {
+                                AttributeSet attr = Xml.asAttributeSet(parser);
+                                if ((type == XmlPullParser.START_TAG)) {
+                                    if (WORKSPACE_TOP_PADDING.equals(parser.getName())) {
+                                        workspaceTopPadding = new PaddingFormula(context, attr);
+                                    } else if (WORKSPACE_BOTTOM_PADDING.equals(parser.getName())) {
+                                        workspaceBottomPadding = new PaddingFormula(context, attr);
+                                    } else if (HOTSEAT_BOTTOM_PADDING.equals(parser.getName())) {
+                                        hotseatBottomPadding = new PaddingFormula(context, attr);
+                                    }
+                                }
+                            }
+
+                            if (workspaceTopPadding == null
+                                    || workspaceBottomPadding == null
+                                    || hotseatBottomPadding == null) {
+                                throw new RuntimeException("DevicePadding missing padding.");
+                            }
+
+                            mDevicePaddings.add(new DevicePadding(maxWidthPx, workspaceTopPadding,
+                                    workspaceBottomPadding, hotseatBottomPadding));
+                        }
+                    }
+                }
+            }
+        } catch (IOException | XmlPullParserException e) {
+            throw new RuntimeException(e);
+        }
+
+        // Sort ascending by maxEmptySpacePx
+        mDevicePaddings.sort((sl1, sl2) -> Integer.compare(sl1.maxEmptySpacePx,
+                sl2.maxEmptySpacePx));
+    }
+
+    public DevicePadding getDevicePadding(int extraSpacePx) {
+        for (DevicePadding limit : mDevicePaddings) {
+            if (extraSpacePx <= limit.maxEmptySpacePx) {
+                return limit;
+            }
+        }
+
+        return mDevicePaddings.get(mDevicePaddings.size() - 1);
+    }
+
+    /**
+     * Holds all the formulas to calculate the padding for a particular device based on the
+     * amount of extra space.
+     */
+    public static final class DevicePadding {
+
+        private final int maxEmptySpacePx;
+        private final PaddingFormula workspaceTopPadding;
+        private final PaddingFormula workspaceBottomPadding;
+        private final PaddingFormula hotseatBottomPadding;
+
+        public DevicePadding(int maxEmptySpacePx,
+                PaddingFormula workspaceTopPadding,
+                PaddingFormula workspaceBottomPadding,
+                PaddingFormula hotseatBottomPadding) {
+            this.maxEmptySpacePx = maxEmptySpacePx;
+            this.workspaceTopPadding = workspaceTopPadding;
+            this.workspaceBottomPadding = workspaceBottomPadding;
+            this.hotseatBottomPadding = hotseatBottomPadding;
+        }
+
+        public int getWorkspaceTopPadding(int extraSpacePx) {
+            return workspaceTopPadding.calculate(extraSpacePx);
+        }
+
+        public int getWorkspaceBottomPadding(int extraSpacePx) {
+            return workspaceBottomPadding.calculate(extraSpacePx);
+        }
+
+        public int getHotseatBottomPadding(int extraSpacePx) {
+            return hotseatBottomPadding.calculate(extraSpacePx);
+        }
+    }
+
+    /**
+     * Used to calculate a padding based on three variables: a, b, and c.
+     *
+     * Calculation: a * (extraSpace - c) + b
+     */
+    private static final class PaddingFormula {
+
+        private final float a;
+        private final float b;
+        private final float c;
+
+        public PaddingFormula(Context context, AttributeSet attrs) {
+            TypedArray t = context.obtainStyledAttributes(attrs,
+                    R.styleable.DevicePaddingFormula);
+
+            a = getValue(t, R.styleable.DevicePaddingFormula_a);
+            b = getValue(t, R.styleable.DevicePaddingFormula_b);
+            c = getValue(t, R.styleable.DevicePaddingFormula_c);
+
+            t.recycle();
+        }
+
+        public int calculate(int extraSpacePx) {
+            if (DEBUG) {
+                Log.d(TAG, "a=" + a + " * (" + extraSpacePx + " - " + c + ") + b=" + b);
+            }
+            return Math.round(a * (extraSpacePx - c) + b);
+        }
+
+        private static float getValue(TypedArray a, int index) {
+            if (a.getType(index) == TypedValue.TYPE_DIMENSION) {
+                return a.getDimensionPixelSize(index, 0);
+            } else if (a.getType(index) == TypedValue.TYPE_FLOAT) {
+                return a.getFloat(index, 0);
+            }
+            return 0;
+        }
+
+        @Override
+        public String toString() {
+            return "a=" + a + ", b=" + b + ", c=" + c;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index f681d75..f2dd60e 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static com.android.launcher3.ResourceUtils.pxFromDp;
+
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -23,8 +25,12 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.view.Surface;
+import android.view.WindowInsets;
+import android.view.WindowManager;
 
 import com.android.launcher3.CellLayout.ContainerType;
+import com.android.launcher3.DevicePaddings.DevicePadding;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.IconShape;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.icons.IconNormalizer;
@@ -60,6 +66,8 @@
 
     public final float aspectRatio;
 
+    public final boolean isScalableGrid;
+
     /**
      * The maximum amount of left/right workspace padding as a percentage of the screen width.
      * To be clear, this means that up to 7% of the screen width can be used as left padding, and
@@ -73,13 +81,19 @@
     private static final int PORTRAIT_TABLET_LEFT_RIGHT_PADDING_MULTIPLIER = 4;
 
     // Workspace
-    public final int desiredWorkspaceLeftRightMarginPx;
+    public final int desiredWorkspaceLeftRightOriginalPx;
+    public int desiredWorkspaceLeftRightMarginPx;
+    public final int cellLayoutBorderSpacingOriginalPx;
+    public int cellLayoutBorderSpacingPx;
     public final int cellLayoutPaddingLeftRightPx;
     public final int cellLayoutBottomPaddingPx;
     public final int edgeMarginPx;
     public float workspaceSpringLoadShrinkFactor;
     public final int workspaceSpringLoadedBottomSpace;
 
+    public int workspaceTopPadding;
+    public int workspaceBottomPadding;
+
     // Workspace page indicator
     public final int workspacePageIndicatorHeight;
     private final int mWorkspacePageIndicatorOverlapWorkspace;
@@ -94,10 +108,19 @@
     public int cellHeightPx;
     public int workspaceCellPaddingXPx;
 
+    public int cellYPaddingPx;
+    public int cellYPaddingOriginalPx;
+
     // Folder
+    public float folderLabelTextScale;
+    public int folderLabelTextSizePx;
     public int folderIconSizePx;
     public int folderIconOffsetYPx;
 
+    // Folder content
+    public int folderContentPaddingLeftRight;
+    public int folderContentPaddingTop;
+
     // Folder cell
     public int folderCellWidthPx;
     public int folderCellHeightPx;
@@ -141,6 +164,10 @@
     public DotRenderer mDotRendererWorkSpace;
     public DotRenderer mDotRendererAllApps;
 
+     // Taskbar
+    public boolean isTaskbarPresent;
+    public int taskbarSize;
+
     DeviceProfile(Context context, InvariantDeviceProfile inv, Info info,
             Point minSize, Point maxSize, int width, int height, boolean isLandscape,
             boolean isMultiWindowMode, boolean transposeLayoutWithOrientation,
@@ -149,18 +176,22 @@
         this.inv = inv;
         this.isLandscape = isLandscape;
         this.isMultiWindowMode = isMultiWindowMode;
+        this.transposeLayoutWithOrientation = transposeLayoutWithOrientation;
         windowX = windowPosition.x;
         windowY = windowPosition.y;
 
+        isScalableGrid = inv.isScalable && !isVerticalBarLayout() && !isMultiWindowMode;
+
         // Determine sizes.
         widthPx = width;
         heightPx = height;
+        int nonFinalAvailableHeightPx;
         if (isLandscape) {
             availableWidthPx = maxSize.x;
-            availableHeightPx = minSize.y;
+            nonFinalAvailableHeightPx = minSize.y;
         } else {
             availableWidthPx = minSize.x;
-            availableHeightPx = maxSize.y;
+            nonFinalAvailableHeightPx = maxSize.y;
         }
 
         mInfo = info;
@@ -168,26 +199,56 @@
         // Constants from resources
         float swDPs = Utilities.dpiFromPx(
                 Math.min(info.smallestSize.x, info.smallestSize.y), info.metrics);
-        isTablet = swDPs >= TABLET_MIN_DPS;
-        isLargeTablet = swDPs >= LARGE_TABLET_MIN_DPS;
+        boolean allowRotation = context.getResources().getBoolean(R.bool.allow_rotation);
+        // Tablet UI is built with assumption that simulated landscape is disabled.
+        isTablet = allowRotation && swDPs >= TABLET_MIN_DPS;
+        isLargeTablet = isTablet && swDPs >= LARGE_TABLET_MIN_DPS;
         isPhone = !isTablet && !isLargeTablet;
         aspectRatio = ((float) Math.max(widthPx, heightPx)) / Math.min(widthPx, heightPx);
         boolean isTallDevice = Float.compare(aspectRatio, TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0;
 
         // Some more constants
-        this.transposeLayoutWithOrientation = transposeLayoutWithOrientation;
-
         context = getContext(context, info, isVerticalBarLayout()
                 ? Configuration.ORIENTATION_LANDSCAPE
                 : Configuration.ORIENTATION_PORTRAIT);
         final Resources res = context.getResources();
 
+        isTaskbarPresent = isTablet && FeatureFlags.ENABLE_TASKBAR.get();
+        if (isTaskbarPresent) {
+            // Taskbar will be added later, but provides bottom insets that we should subtract
+            // from availableHeightPx.
+            taskbarSize = res.getDimensionPixelSize(R.dimen.taskbar_size);
+            WindowInsets windowInsets = DisplayController.INSTANCE.get(context).getHolder(mInfo.id)
+                    .getDisplayContext().getSystemService(WindowManager.class)
+                    .getCurrentWindowMetrics().getWindowInsets();
+            int nonOverlappingTaskbarInset =
+                    taskbarSize - windowInsets.getSystemWindowInsetBottom();
+            if (nonOverlappingTaskbarInset > 0) {
+                nonFinalAvailableHeightPx -= nonOverlappingTaskbarInset;
+            }
+        }
+        availableHeightPx = nonFinalAvailableHeightPx;
+
         edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
-        desiredWorkspaceLeftRightMarginPx = isVerticalBarLayout() ? 0 : edgeMarginPx;
+
+        desiredWorkspaceLeftRightMarginPx = isVerticalBarLayout() ? 0 : isScalableGrid
+                ? res.getDimensionPixelSize(R.dimen.scalable_grid_left_right_margin)
+                : res.getDimensionPixelSize(R.dimen.dynamic_grid_left_right_margin);
+        desiredWorkspaceLeftRightOriginalPx = desiredWorkspaceLeftRightMarginPx;
+
+        folderLabelTextScale = res.getFloat(R.dimen.folder_label_text_scale);
+        folderContentPaddingLeftRight =
+                res.getDimensionPixelSize(R.dimen.folder_content_padding_left_right);
+        folderContentPaddingTop = res.getDimensionPixelSize(R.dimen.folder_content_padding_top);
+
+        setCellLayoutBorderSpacing(pxFromDp(inv.borderSpacing, mInfo.metrics, 1f));
+        cellLayoutBorderSpacingOriginalPx = cellLayoutBorderSpacingPx;
 
         int cellLayoutPaddingLeftRightMultiplier = !isVerticalBarLayout() && isTablet
                 ? PORTRAIT_TABLET_LEFT_RIGHT_PADDING_MULTIPLIER : 1;
-        int cellLayoutPadding = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_padding);
+        int cellLayoutPadding = isScalableGrid
+                ? 0
+                : res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_padding);
         if (isLandscape) {
             cellLayoutPaddingLeftRightPx = 0;
             cellLayoutBottomPaddingPx = cellLayoutPadding;
@@ -218,22 +279,31 @@
                 res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_side_padding);
         // Add a bit of space between nav bar and hotseat in vertical bar layout.
         hotseatBarSidePaddingStartPx = isVerticalBarLayout() ? workspacePageIndicatorHeight : 0;
-        hotseatBarSizePx = ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics)
+        int hotseatExtraVerticalSize =
+                res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size);
+        hotseatBarSizePx = pxFromDp(inv.iconSize, mInfo.metrics, 1f)
                 + (isVerticalBarLayout()
                 ? (hotseatBarSidePaddingStartPx + hotseatBarSidePaddingEndPx)
-                : (res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size)
-                        + hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx));
+                : (hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx
+                        + (isScalableGrid ? 0 : hotseatExtraVerticalSize)));
 
         // Calculate all of the remaining variables.
-        updateAvailableDimensions(res);
-
+        int extraSpace = updateAvailableDimensions(res);
         // Now that we have all of the variables calculated, we can tune certain sizes.
-        if (!isVerticalBarLayout() && isPhone && isTallDevice) {
+        if (isScalableGrid) {
+            DevicePadding padding = inv.devicePaddings.getDevicePadding(extraSpace);
+            workspaceTopPadding = padding.getWorkspaceTopPadding(extraSpace);
+            workspaceBottomPadding = padding.getWorkspaceBottomPadding(extraSpace);
+
+            float hotseatBarBottomPadding = padding.getHotseatBottomPadding(extraSpace);
+            hotseatBarSizePx += hotseatBarBottomPadding;
+            hotseatBarBottomPaddingPx += hotseatBarBottomPadding;
+        } else if (!isVerticalBarLayout() && isPhone && isTallDevice) {
             // We increase the hotseat size when there is extra space.
             // ie. For a display with a large aspect ratio, we can keep the icons on the workspace
             // in portrait mode closer together by adding more height to the hotseat.
             // Note: This calculation was created after noticing a pattern in the design spec.
-            int extraSpace = getCellSize().y - iconSizePx - iconDrawablePaddingPx * 2
+            extraSpace = getCellSize().y - iconSizePx - iconDrawablePaddingPx * 2
                     - workspacePageIndicatorHeight;
             hotseatBarSizePx += extraSpace;
             hotseatBarBottomPaddingPx += extraSpace;
@@ -251,6 +321,30 @@
                         IconShape.DEFAULT_PATH_SIZE);
     }
 
+    private void setCellLayoutBorderSpacing(int borderSpacing) {
+        if (isScalableGrid) {
+            cellLayoutBorderSpacingPx = borderSpacing;
+            folderContentPaddingLeftRight = borderSpacing;
+            folderContentPaddingTop = borderSpacing;
+        } else {
+            cellLayoutBorderSpacingPx = 0;
+        }
+    }
+
+    /**
+     * We inset the widget padding added by the system and instead rely on the border spacing
+     * between cells to create reliable consistency between widgets
+     */
+    public boolean shouldInsetWidgets() {
+        Rect widgetPadding = inv.defaultWidgetPadding;
+
+        // Check all sides to ensure that the widget won't overlap into another cell.
+        return cellLayoutBorderSpacingPx > widgetPadding.left
+                && cellLayoutBorderSpacingPx > widgetPadding.top
+                && cellLayoutBorderSpacingPx > widgetPadding.right
+                && cellLayoutBorderSpacingPx > widgetPadding.bottom;
+    }
+
     public Builder toBuilder(Context context) {
         Point size = new Point(availableWidthPx, availableHeightPx);
         return new Builder(context, inv, mInfo)
@@ -326,17 +420,46 @@
                 + topBottomPadding * 2;
     }
 
-    private void updateAvailableDimensions(Resources res) {
+    /**
+     * Returns the amount of extra (or unused) vertical space.
+     */
+    private int updateAvailableDimensions(Resources res) {
         updateIconSize(1f, res);
 
-        // Check to see if the icons fit within the available height.  If not, then scale down.
-        float usedHeight = (cellHeightPx * inv.numRows);
-        int maxHeight = (availableHeightPx - getTotalWorkspacePadding().y);
-        if (usedHeight > maxHeight) {
-            float scale = maxHeight / usedHeight;
-            updateIconSize(scale, res);
+        Point workspacePadding = getTotalWorkspacePadding();
+
+        // Check to see if the icons fit within the available height.
+        float usedHeight = getCellLayoutHeight();
+        final int maxHeight = availableHeightPx - workspacePadding.y;
+        float extraHeight = Math.max(0, maxHeight - usedHeight);
+        float scaleY = maxHeight / usedHeight;
+        boolean shouldScale = scaleY < 1f;
+
+        float scaleX = 1f;
+        if (isScalableGrid) {
+            // We scale to fit the cellWidth and cellHeight in the available space.
+            // The benefit of scalable grids is that we can get consistent aspect ratios between
+            // devices.
+            float usedWidth = (cellWidthPx * inv.numColumns)
+                    + (cellLayoutBorderSpacingPx * (inv.numColumns - 1))
+                    + (desiredWorkspaceLeftRightMarginPx * 2);
+            // We do not subtract padding here, as we also scale the workspace padding if needed.
+            scaleX = availableWidthPx / usedWidth;
+            shouldScale = true;
         }
+
+        if (shouldScale) {
+            float scale = Math.min(scaleX, scaleY);
+            updateIconSize(scale, res);
+            extraHeight = Math.max(0, maxHeight - getCellLayoutHeight());
+        }
+
         updateAvailableFolderCellDimensions(res);
+        return Math.round(extraHeight);
+    }
+
+    private int getCellLayoutHeight() {
+        return (cellHeightPx * inv.numRows) + (cellLayoutBorderSpacingPx * (inv.numRows - 1));
     }
 
     /**
@@ -344,31 +467,41 @@
      * iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants,
      * hotseat sizes, workspaceSpringLoadedShrinkFactor, folderIconSizePx, and folderIconOffsetYPx.
      */
-    private void updateIconSize(float scale, Resources res) {
+    public void updateIconSize(float scale, Resources res) {
         // Workspace
         final boolean isVerticalLayout = isVerticalBarLayout();
         float invIconSizeDp = isVerticalLayout ? inv.landscapeIconSize : inv.iconSize;
-        iconSizePx = Math.max(1, (int) (ResourceUtils.pxFromDp(invIconSizeDp, mInfo.metrics)
-                * scale));
-        iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, mInfo.metrics) * scale);
+        iconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mInfo.metrics, scale));
+        iconTextSizePx = pxFromDp(inv.iconTextSize, mInfo.metrics, scale);
         iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * scale);
 
-        cellHeightPx = iconSizePx + iconDrawablePaddingPx
-                + Utilities.calculateTextHeight(iconTextSizePx);
-        int cellYPadding = (getCellSize().y - cellHeightPx) / 2;
-        if (iconDrawablePaddingPx > cellYPadding && !isVerticalLayout
-                && !isMultiWindowMode) {
-            // Ensures that the label is closer to its corresponding icon. This is not an issue
-            // with vertical bar layout or multi-window mode since the issue is handled separately
-            // with their calls to {@link #adjustToHideWorkspaceLabels}.
-            cellHeightPx -= (iconDrawablePaddingPx - cellYPadding);
-            iconDrawablePaddingPx = cellYPadding;
+        setCellLayoutBorderSpacing((int) (cellLayoutBorderSpacingOriginalPx * scale));
+
+        if (isScalableGrid) {
+            cellWidthPx = pxFromDp(inv.minCellWidth, mInfo.metrics, scale);
+            cellHeightPx = pxFromDp(inv.minCellHeight, mInfo.metrics, scale);
+            int cellContentHeight = iconSizePx + iconDrawablePaddingPx
+                    + Utilities.calculateTextHeight(iconTextSizePx);
+            cellYPaddingPx = Math.max(0, cellHeightPx - cellContentHeight) / 2;
+            desiredWorkspaceLeftRightMarginPx = (int) (desiredWorkspaceLeftRightOriginalPx * scale);
+        } else {
+            cellWidthPx = iconSizePx + iconDrawablePaddingPx;
+            cellHeightPx = iconSizePx + iconDrawablePaddingPx
+                    + Utilities.calculateTextHeight(iconTextSizePx);
+            int cellPaddingY = (getCellSize().y - cellHeightPx) / 2;
+            if (iconDrawablePaddingPx > cellPaddingY && !isVerticalLayout
+                    && !isMultiWindowMode) {
+                // Ensures that the label is closer to its corresponding icon. This is not an issue
+                // with vertical bar layout or multi-window mode since the issue is handled
+                // separately with their calls to {@link #adjustToHideWorkspaceLabels}.
+                cellHeightPx -= (iconDrawablePaddingPx - cellPaddingY);
+                iconDrawablePaddingPx = cellPaddingY;
+            }
         }
-        cellWidthPx = iconSizePx + iconDrawablePaddingPx;
 
         // All apps
         if (allAppsHasDifferentNumColumns()) {
-            allAppsIconSizePx = ResourceUtils.pxFromDp(inv.allAppsIconSize, mInfo.metrics);
+            allAppsIconSizePx = pxFromDp(inv.allAppsIconSize, mInfo.metrics);
             allAppsIconTextSizePx = Utilities.pxFromSp(inv.allAppsIconTextSize, mInfo.metrics);
             allAppsIconDrawablePaddingPx = iconDrawablePaddingOriginalPx;
             // We use 4 below to ensure labels are closer to their corresponding icon.
@@ -412,25 +545,26 @@
     }
 
     private void updateAvailableFolderCellDimensions(Resources res) {
-        int folderBottomPanelSize = res.getDimensionPixelSize(R.dimen.folder_label_padding_top)
-                + res.getDimensionPixelSize(R.dimen.folder_label_padding_bottom)
-                + Utilities.calculateTextHeight(res.getDimension(R.dimen.folder_label_text_size));
-
         updateFolderCellSize(1f, res);
 
+        final int folderBottomPanelSize = res.getDimensionPixelSize(R.dimen.folder_label_height);
+
         // Don't let the folder get too close to the edges of the screen.
         int folderMargin = edgeMarginPx * 2;
         Point totalWorkspacePadding = getTotalWorkspacePadding();
 
         // Check if the icons fit within the available height.
-        float contentUsedHeight = folderCellHeightPx * inv.numFolderRows;
+        float contentUsedHeight = folderCellHeightPx * inv.numFolderRows
+                + ((inv.numFolderRows - 1) * cellLayoutBorderSpacingPx);
         int contentMaxHeight = availableHeightPx - totalWorkspacePadding.y - folderBottomPanelSize
-                - folderMargin;
+                - folderMargin - folderContentPaddingTop;
         float scaleY = contentMaxHeight / contentUsedHeight;
 
         // Check if the icons fit within the available width.
-        float contentUsedWidth = folderCellWidthPx * inv.numFolderColumns;
-        int contentMaxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin;
+        float contentUsedWidth = folderCellWidthPx * inv.numFolderColumns
+                + ((inv.numFolderColumns - 1) * cellLayoutBorderSpacingPx);
+        int contentMaxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin
+                - folderContentPaddingLeftRight * 2;
         float scaleX = contentMaxWidth / contentUsedWidth;
 
         float scale = Math.min(scaleX, scaleY);
@@ -440,9 +574,10 @@
     }
 
     private void updateFolderCellSize(float scale, Resources res) {
-        folderChildIconSizePx = (int) (ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics) * scale);
-        folderChildTextSizePx =
-                (int) (res.getDimensionPixelSize(R.dimen.folder_child_text_size) * scale);
+        float invIconSizeDp = isVerticalBarLayout() ? inv.landscapeIconSize : inv.iconSize;
+        folderChildIconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mInfo.metrics, scale));
+        folderChildTextSizePx = pxFromDp(inv.iconTextSize, mInfo.metrics, scale);
+        folderLabelTextSizePx = (int) (folderChildTextSizePx * folderLabelTextScale);
 
         int textHeight = Utilities.calculateTextHeight(folderChildTextSizePx);
         int cellPaddingX = (int) (res.getDimensionPixelSize(R.dimen.folder_cell_x_padding) * scale);
@@ -477,9 +612,9 @@
         // not matter.
         Point padding = getTotalWorkspacePadding();
         result.x = calculateCellWidth(availableWidthPx - padding.x
-                - cellLayoutPaddingLeftRightPx * 2, numColumns);
+                - cellLayoutPaddingLeftRightPx * 2, cellLayoutBorderSpacingPx, numColumns);
         result.y = calculateCellHeight(availableHeightPx - padding.y
-                - cellLayoutBottomPaddingPx, numRows);
+                - cellLayoutBottomPaddingPx, cellLayoutBorderSpacingPx, numRows);
         return result;
     }
 
@@ -506,8 +641,9 @@
                 padding.right = hotseatBarSizePx;
             }
         } else {
-            int paddingBottom = hotseatBarSizePx + workspacePageIndicatorHeight
-                    - mWorkspacePageIndicatorOverlapWorkspace;
+            int hotseatTop = isTaskbarPresent ? taskbarSize : hotseatBarSizePx;
+            int paddingBottom = hotseatTop + workspacePageIndicatorHeight
+                    + workspaceBottomPadding - mWorkspacePageIndicatorOverlapWorkspace;
             if (isTablet) {
                 // Pad the left and right of the workspace to ensure consistent spacing
                 // between all icons
@@ -516,15 +652,16 @@
                         ((inv.numColumns - 1) * cellWidthPx)));
                 availablePaddingX = (int) Math.min(availablePaddingX,
                         widthPx * MAX_HORIZONTAL_PADDING_PERCENT);
+                int hotseatVerticalPadding = isTaskbarPresent ? 0
+                        : hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx;
                 int availablePaddingY = Math.max(0, heightPx - edgeMarginPx - paddingBottom
-                        - (2 * inv.numRows * cellHeightPx) - hotseatBarTopPaddingPx
-                        - hotseatBarBottomPaddingPx);
+                        - (2 * inv.numRows * cellHeightPx) - hotseatVerticalPadding);
                 padding.set(availablePaddingX / 2, edgeMarginPx + availablePaddingY / 2,
                         availablePaddingX / 2, paddingBottom + availablePaddingY / 2);
             } else {
                 // Pad the top and bottom of the workspace with search/hotseat bar sizes
                 padding.set(desiredWorkspaceLeftRightMarginPx,
-                        edgeMarginPx,
+                        workspaceTopPadding + (isScalableGrid ? 0 : edgeMarginPx),
                         desiredWorkspaceLeftRightMarginPx,
                         paddingBottom);
             }
@@ -579,11 +716,11 @@
         }
     }
 
-    public static int calculateCellWidth(int width, int countX) {
-        return width / countX;
+    public static int calculateCellWidth(int width, int borderSpacing, int countX) {
+        return (width - ((countX - 1) * borderSpacing)) / countX;
     }
-    public static int calculateCellHeight(int height, int countY) {
-        return height / countY;
+    public static int calculateCellHeight(int height, int borderSpacing, int countY) {
+        return (height - ((countY - 1) * borderSpacing)) / countY;
     }
 
     /**
@@ -608,8 +745,13 @@
     public boolean updateIsSeascape(Context context) {
         if (isVerticalBarLayout()) {
             // Check an up-to-date info.
-            boolean isSeascape = DisplayController.getDefaultDisplay(context)
-                    .createInfoForContext(context).rotation == Surface.ROTATION_270;
+            DisplayController.Info displayInfo = DisplayController.getDefaultDisplay(context)
+                    .createInfoForContext(context);
+            if (displayInfo == null) {
+                return false;
+            }
+
+            boolean isSeascape = displayInfo.rotation == Surface.ROTATION_270;
             if (mIsSeascape != isSeascape) {
                 mIsSeascape = isSeascape;
                 return true;
diff --git a/src/com/android/launcher3/DropTargetBar.java b/src/com/android/launcher3/DropTargetBar.java
index ca001a3..c768493 100644
--- a/src/com/android/launcher3/DropTargetBar.java
+++ b/src/com/android/launcher3/DropTargetBar.java
@@ -131,7 +131,10 @@
         int width = MeasureSpec.getSize(widthMeasureSpec);
         int height = MeasureSpec.getSize(heightMeasureSpec);
 
-        if (mIsVertical) {
+        int visibleCount = getVisibleButtonsCount();
+        if (visibleCount == 0) {
+            // do nothing
+        } else if (mIsVertical) {
             int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
             int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
 
@@ -142,7 +145,6 @@
                 }
             }
         } else {
-            int visibleCount = getVisibleButtonsCount();
             int availableWidth = width / visibleCount;
             boolean textVisible = true;
             for (ButtonDropTarget buttons : mDropTargets) {
@@ -165,7 +167,10 @@
 
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        if (mIsVertical) {
+        int visibleCount = getVisibleButtonsCount();
+        if (visibleCount == 0) {
+            // do nothing
+        } else if (mIsVertical) {
             int gap = getResources().getDimensionPixelSize(R.dimen.drop_target_vertical_gap);
             int start = gap;
             int end;
@@ -178,7 +183,6 @@
                 }
             }
         } else {
-            int visibleCount = getVisibleButtonsCount();
             int frameSize = (right - left) / visibleCount;
 
             int start = frameSize / 2;
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 4f4f2a7..b2112ad 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -21,6 +21,7 @@
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.MotionEvent;
+import android.view.View;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
@@ -98,14 +99,20 @@
         } else {
             lp.gravity = Gravity.BOTTOM;
             lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
-            lp.height = grid.hotseatBarSizePx + insets.bottom;
+            lp.height = grid.isTaskbarPresent
+                    ? grid.taskbarSize
+                    : grid.hotseatBarSizePx + insets.bottom;
         }
-        Rect padding = grid.getHotseatLayoutPadding();
-        int paddingBottom = padding.bottom;
-        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && !grid.isVerticalBarLayout()) {
-            paddingBottom -= grid.hotseatBarBottomPaddingPx;
+        if (!grid.isTaskbarPresent) {
+            // When taskbar is present, we set the padding separately to ensure a seamless visual
+            // handoff between taskbar and hotseat during drag and drop.
+            Rect padding = grid.getHotseatLayoutPadding();
+            int paddingBottom = padding.bottom;
+            if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && !grid.isVerticalBarLayout()) {
+                paddingBottom -= grid.hotseatBarBottomPaddingPx;
+            }
+            setPadding(padding.left, padding.top, padding.right, paddingBottom);
         }
-        setPadding(padding.left, padding.top, padding.right, paddingBottom);
 
         setLayoutParams(lp);
         InsettableFrameLayout.dispatchInsets(this, insets);
@@ -160,4 +167,11 @@
     protected void showInlineQsb() {
         //Does nothing
     }
+
+    /**
+     * Returns the first View for which the given itemOperator returns true, or null.
+     */
+    public View getFirstItemMatch(Workspace.ItemOperator itemOperator) {
+        return mWorkspace.getFirstMatch(new CellLayout[] { this }, itemOperator);
+    }
 }
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 5fd9e01..bb60557 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -18,7 +18,6 @@
 
 import static com.android.launcher3.Utilities.getDevicePrefs;
 import static com.android.launcher3.Utilities.getPointString;
-import static com.android.launcher3.config.FeatureFlags.APPLY_CONFIG_AT_RUNTIME;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_FOUR_COLUMNS;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.PackageManagerHelper.getPackageFilter;
@@ -112,6 +111,10 @@
     public float allAppsIconSize;
     public float allAppsIconTextSize;
 
+    public float minCellHeight;
+    public float minCellWidth;
+    public float borderSpacing;
+
     private SparseArray<TypedValue> mExtraAttrs;
 
     /**
@@ -124,6 +127,11 @@
      */
     public int numAllAppsColumns;
 
+    /**
+     * Do not query directly. see {@link DeviceProfile#isScalableGrid}.
+     */
+    protected boolean isScalable;
+
     public String dbFile;
     public int defaultLayoutId;
     int demoModeLayoutId;
@@ -131,6 +139,8 @@
     public DeviceProfile landscapeProfile;
     public DeviceProfile portraitProfile;
 
+    public DevicePaddings devicePaddings;
+
     public Point defaultWallpaperSize;
     public Rect defaultWidgetPadding;
 
@@ -153,6 +163,10 @@
         iconTextSize = p.iconTextSize;
         numHotseatIcons = p.numHotseatIcons;
         numAllAppsColumns = p.numAllAppsColumns;
+        isScalable = p.isScalable;
+        minCellHeight = p.minCellHeight;
+        minCellWidth = p.minCellWidth;
+        borderSpacing = p.borderSpacing;
         dbFile = p.dbFile;
         allAppsIconSize = p.allAppsIconSize;
         allAppsIconTextSize = p.allAppsIconTextSize;
@@ -160,6 +174,7 @@
         demoModeLayoutId = p.demoModeLayoutId;
         mExtraAttrs = p.mExtraAttrs;
         mOverlayMonitor = p.mOverlayMonitor;
+        devicePaddings = p.devicePaddings;
     }
 
     @TargetApi(23)
@@ -174,8 +189,7 @@
                 .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, getPointString(numColumns, numRows))
                 .apply();
 
-        mConfigMonitor = new ConfigMonitor(context,
-                APPLY_CONFIG_AT_RUNTIME.get() ? this::onConfigChanged : this::killProcess);
+        mConfigMonitor = new ConfigMonitor(context, this::onConfigChanged);
         mOverlayMonitor = new OverlayMonitor(context);
     }
 
@@ -212,6 +226,11 @@
         result.landscapeIconSize = defaultDisplayOption.landscapeIconSize;
         result.allAppsIconSize = Math.min(
                 defaultDisplayOption.allAppsIconSize, myDisplayOption.allAppsIconSize);
+        result.minCellHeight = defaultDisplayOption.minCellHeight;
+        result.minCellWidth = defaultDisplayOption.minCellWidth;
+        result.borderSpacing = defaultDisplayOption.borderSpacing;
+
+        devicePaddings = new DevicePaddings(context);
         initGrid(context, myInfo, result);
     }
 
@@ -239,6 +258,7 @@
         ArrayList<DisplayOption> allOptions = getPredefinedDeviceProfiles(context, gridName);
 
         DisplayOption displayOption = invDistWeightedInterpolate(displayInfo, allOptions);
+        devicePaddings = new DevicePaddings(context);
         initGrid(context, displayInfo, displayOption);
         return displayOption.grid.name;
     }
@@ -255,6 +275,7 @@
         numFolderRows = closestProfile.numFolderRows;
         numFolderColumns = closestProfile.numFolderColumns;
         numAllAppsColumns = closestProfile.numAllAppsColumns;
+        isScalable = closestProfile.isScalable;
 
         mExtraAttrs = closestProfile.extraAttrs;
 
@@ -265,6 +286,10 @@
         iconTextSize = displayOption.iconTextSize;
         fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
 
+        minCellHeight = displayOption.minCellHeight;
+        minCellWidth = displayOption.minCellWidth;
+        borderSpacing = displayOption.borderSpacing;
+
         if (Utilities.isGridOptionsEnabled(context)) {
             allAppsIconSize = displayOption.allAppsIconSize;
             allAppsIconTextSize = displayOption.allAppsIconTextSize;
@@ -317,11 +342,6 @@
         mChangeListeners.remove(listener);
     }
 
-    private void killProcess(Context context) {
-        Log.e("ConfigMonitor", "restarting launcher");
-        android.os.Process.killProcess(android.os.Process.myPid());
-    }
-
     public void verifyConfigChangedInBackground(final Context context) {
         String savedIconMaskPath = getDevicePrefs(context).getString(KEY_ICON_PATH_REF, "");
         // Good place to check if grid size changed in themepicker when launcher was dead.
@@ -590,6 +610,8 @@
         private final int defaultLayoutId;
         private final int demoModeLayoutId;
 
+        private final boolean isScalable;
+
         private final SparseArray<TypedValue> extraAttrs;
 
         public GridOption(Context context, AttributeSet attrs) {
@@ -613,6 +635,9 @@
             numAllAppsColumns = a.getInt(
                     R.styleable.GridDisplayOption_numAllAppsColumns, numColumns);
 
+            isScalable = a.getBoolean(
+                    R.styleable.GridDisplayOption_isScalable, false);
+
             a.recycle();
 
             extraAttrs = Themes.createValueMap(context, attrs,
@@ -627,6 +652,10 @@
         private final float minHeightDps;
         private final boolean canBeDefault;
 
+        private float minCellHeight;
+        private float minCellWidth;
+        private float borderSpacing;
+
         private float iconSize;
         private float iconTextSize;
         private float landscapeIconSize;
@@ -644,6 +673,10 @@
             canBeDefault = a.getBoolean(
                     R.styleable.ProfileDisplayOption_canBeDefault, false);
 
+            minCellHeight = a.getFloat(R.styleable.ProfileDisplayOption_minCellHeightDps, 0);
+            minCellWidth = a.getFloat(R.styleable.ProfileDisplayOption_minCellWidthDps, 0);
+            borderSpacing = a.getFloat(R.styleable.ProfileDisplayOption_borderSpacingDps, 0);
+
             iconSize = a.getFloat(R.styleable.ProfileDisplayOption_iconImageSize, 0);
             landscapeIconSize = a.getFloat(R.styleable.ProfileDisplayOption_landscapeIconSize,
                     iconSize);
@@ -665,6 +698,9 @@
             minWidthDps = 0;
             minHeightDps = 0;
             canBeDefault = false;
+            minCellHeight = 0;
+            minCellWidth = 0;
+            borderSpacing = 0;
         }
 
         private DisplayOption multiply(float w) {
@@ -673,6 +709,9 @@
             allAppsIconSize *= w;
             iconTextSize *= w;
             allAppsIconTextSize *= w;
+            minCellHeight *= w;
+            minCellWidth *= w;
+            borderSpacing *= w;
             return this;
         }
 
@@ -682,6 +721,9 @@
             allAppsIconSize += p.allAppsIconSize;
             iconTextSize += p.iconTextSize;
             allAppsIconTextSize += p.allAppsIconTextSize;
+            minCellHeight += p.minCellHeight;
+            minCellWidth += p.minCellWidth;
+            borderSpacing += p.borderSpacing;
             return this;
         }
     }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 0274775..fa63885 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -37,9 +37,13 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
 import static com.android.launcher3.Utilities.postAsyncCallback;
+import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.getSupportedActions;
 import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_LAUNCHER_LOAD;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_ENTRY;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_ENTRY_WITH_DEVICE_SEARCH;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_EXIT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONRESUME;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ONSTOP;
 import static com.android.launcher3.model.ItemInstallQueue.FLAG_ACTIVITY_PAUSED;
@@ -72,6 +76,7 @@
 import android.content.res.Configuration;
 import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Bitmap;
+import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.CancellationSignal;
@@ -98,16 +103,13 @@
 import android.widget.Toast;
 
 import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
 import androidx.annotation.VisibleForTesting;
-import androidx.lifecycle.Lifecycle;
-import androidx.lifecycle.LifecycleOwner;
-import androidx.lifecycle.LifecycleRegistry;
 
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.LauncherAction;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.allapps.AllAppsTransitionController;
@@ -119,15 +121,17 @@
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.folder.FolderGridOrganizer;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.icons.BitmapRenderer;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.keyboard.CustomActionsPopup;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.ItemInstallQueue;
@@ -167,7 +171,6 @@
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.PendingRequestArgs;
 import com.android.launcher3.util.SafeCloseable;
-import com.android.launcher3.util.ShortcutUtil;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.Thunk;
@@ -179,16 +182,18 @@
 import com.android.launcher3.views.FloatingSurfaceView;
 import com.android.launcher3.views.OptionsPopupView;
 import com.android.launcher3.views.ScrimView;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.PendingAppWidgetHostView;
 import com.android.launcher3.widget.WidgetAddFlowHandler;
 import com.android.launcher3.widget.WidgetHostViewLoader;
-import com.android.launcher3.widget.WidgetListRowEntry;
 import com.android.launcher3.widget.WidgetManagerHelper;
-import com.android.launcher3.widget.WidgetsFullSheet;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
 import com.android.systemui.plugins.OverlayPlugin;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.plugins.shared.LauncherExterns;
@@ -211,8 +216,7 @@
  * Default launcher application.
  */
 public class Launcher extends StatefulActivity<LauncherState> implements LauncherExterns,
-        Callbacks, InvariantDeviceProfile.OnIDPChangeListener, PluginListener<OverlayPlugin>,
-        LifecycleOwner {
+        Callbacks, InvariantDeviceProfile.OnIDPChangeListener, PluginListener<OverlayPlugin> {
     public static final String TAG = "Launcher";
 
     public static final ActivityTracker<Launcher> ACTIVITY_TRACKER = new ActivityTracker<>();
@@ -275,8 +279,6 @@
     private LauncherAppTransitionManager mAppTransitionManager;
     private Configuration mOldConfig;
 
-    private LifecycleRegistry mLifecycleRegistry;
-
     private LiveSearchManager mLiveSearchManager;
 
     @Thunk
@@ -359,6 +361,13 @@
 
     private SafeCloseable mUserChangedCallbackCloseable;
 
+    // New InstanceId is assigned to mAllAppsSessionLogId for each AllApps sessions.
+    // When Launcher is not in AllApps state mAllAppsSessionLogId will be null.
+    // User actions within AllApps state are logged with this InstanceId, to recreate AllApps
+    // session on the server side.
+    protected InstanceId mAllAppsSessionLogId;
+    private LauncherState mPrevLauncherState;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         Object traceToken = TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT,
@@ -390,14 +399,14 @@
         idp.addOnChangeListener(this);
         mSharedPrefs = Utilities.getPrefs(this);
         mIconCache = app.getIconCache();
-        mAccessibilityDelegate = new LauncherAccessibilityDelegate(this);
-
-        mLiveSearchManager = new LiveSearchManager(this);
+        mAccessibilityDelegate = createAccessibilityDelegate();
 
         mDragController = new DragController(this);
         mAllAppsController = new AllAppsTransitionController(this);
         mStateManager = new StateManager<>(this, NORMAL);
 
+        mLiveSearchManager = new LiveSearchManager(this);
+
         mOnboardingPrefs = createOnboardingPrefs(mSharedPrefs);
 
         mAppWidgetManager = new WidgetManagerHelper(this);
@@ -486,15 +495,6 @@
         if (Utilities.ATLEAST_R) {
             getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
         }
-
-        mLifecycleRegistry = new LifecycleRegistry(this);
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
-    }
-
-    @NonNull
-    @Override
-    public Lifecycle getLifecycle() {
-        return mLifecycleRegistry;
     }
 
     public LiveSearchManager getLiveSearchManager() {
@@ -913,7 +913,6 @@
 
     @Override
     protected void onStop() {
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
         super.onStop();
         if (mDeferOverlayCallbacks) {
             checkIfOverlayStillDeferred();
@@ -922,7 +921,7 @@
         }
 
         logStopAndResume(false /* isResume */);
-        mAppWidgetHost.setListenIfResumed(false);
+        mAppWidgetHost.setActivityStarted(false);
         NotificationListener.removeNotificationsChangedListener();
     }
 
@@ -935,9 +934,8 @@
             mOverlayManager.onActivityStarted(this);
         }
 
-        mAppWidgetHost.setListenIfResumed(true);
+        mAppWidgetHost.setActivityStarted(true);
         TraceHelper.INSTANCE.endSection(traceToken);
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
     }
 
     @Override
@@ -956,6 +954,7 @@
         NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
 
         DiscoveryBounce.showForHomeIfNeeded(this);
+        mAppWidgetHost.setActivityResumed(true);
     }
 
     private void logStopAndResume(boolean isResume) {
@@ -1044,12 +1043,13 @@
         }
         // When multiple pages are visible, show persistent page indicator
         mWorkspace.getPageIndicator().setShouldAutoHide(!state.hasFlag(FLAG_MULTI_PAGE));
+        mPrevLauncherState = mStateManager.getCurrentStableState();
     }
 
     @Override
     public void onStateSetEnd(LauncherState state) {
         super.onStateSetEnd(state);
-        getAppWidgetHost().setResumed(state == LauncherState.NORMAL);
+        getAppWidgetHost().setStateIsNormal(state == LauncherState.NORMAL);
         getWorkspace().setClipChildren(!state.hasFlag(FLAG_MULTI_PAGE));
 
         finishAutoCancelActionMode();
@@ -1067,6 +1067,21 @@
             // Clear any rotation locks when going to normal state
             getRotationHelper().setCurrentStateRequest(REQUEST_NONE);
         }
+
+        if (ALL_APPS.equals(state)) {
+            // creates new instance ID since new all apps session is started.
+            mAllAppsSessionLogId = new InstanceIdSequence().newInstanceId();
+            getStatsLogManager()
+                    .logger()
+                    .log(FeatureFlags.ENABLE_DEVICE_SEARCH.get()
+                            ? LAUNCHER_ALLAPPS_ENTRY_WITH_DEVICE_SEARCH
+                            : LAUNCHER_ALLAPPS_ENTRY);
+        } else if (ALL_APPS.equals(mPrevLauncherState)
+                // Check if mLogInstanceId is not null to make sure exit event is logged only once.
+                && mAllAppsSessionLogId != null) {
+            getStatsLogManager().logger().log(LAUNCHER_ALLAPPS_EXIT);
+            mAllAppsSessionLogId = null;
+        }
     }
 
     @Override
@@ -1091,7 +1106,6 @@
         }
 
         TraceHelper.INSTANCE.endSection(traceToken);
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED);
     }
 
     @Override
@@ -1099,7 +1113,6 @@
         // Ensure that items added to Launcher are queued until Launcher returns
         ItemInstallQueue.INSTANCE.get(this).pauseModelPush(FLAG_ACTIVITY_PAUSED);
 
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
         super.onPause();
         mDragController.cancelDrag();
         mLastTouchUpTime = -1;
@@ -1108,6 +1121,7 @@
         if (!mDeferOverlayCallbacks) {
             mOverlayManager.onActivityPaused(this);
         }
+        mAppWidgetHost.setActivityResumed(false);
     }
 
     class LauncherOverlayCallbacksImpl implements LauncherOverlayCallbacks {
@@ -1598,7 +1612,6 @@
         mAppTransitionManager.unregisterRemoteAnimations();
         mAppTransitionManager.unregisterRemoteTransitions();
         mUserChangedCallbackCloseable.close();
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
         mLiveSearchManager.stop();
     }
 
@@ -1793,6 +1806,43 @@
         return newFolder;
     }
 
+    @Override
+    public Rect getFolderBoundingBox() {
+        // We need to bound the folder to the currently visible workspace area
+        Rect folderBoundingBox = new Rect();
+        getWorkspace().getPageAreaRelativeToDragLayer(folderBoundingBox);
+        return folderBoundingBox;
+    }
+
+    @Override
+    public void updateOpenFolderPosition(int[] inOutPosition, Rect bounds, int width, int height) {
+        int left = inOutPosition[0];
+        int top = inOutPosition[1];
+        DeviceProfile grid = getDeviceProfile();
+        int distFromEdgeOfScreen = getWorkspace().getPaddingLeft();
+        if (grid.isPhone && (grid.availableWidthPx - width) < 4 * distFromEdgeOfScreen) {
+            // Center the folder if it is very close to being centered anyway, by virtue of
+            // filling the majority of the viewport. ie. remove it from the uncanny valley
+            // of centeredness.
+            left = (grid.availableWidthPx - width) / 2;
+        } else if (width >= bounds.width()) {
+            // If the folder doesn't fit within the bounds, center it about the desired bounds
+            left = bounds.left + (bounds.width() - width) / 2;
+        }
+        if (height >= bounds.height()) {
+            // Folder height is greater than page height, center on page
+            top = bounds.top + (bounds.height() - height) / 2;
+        } else {
+            // Folder height is less than page height, so bound it to the absolute open folder
+            // bounds if necessary
+            Rect folderBounds = grid.getAbsoluteOpenFolderBounds();
+            left = Math.max(folderBounds.left, Math.min(left, folderBounds.right - width));
+            top = Math.max(folderBounds.top, Math.min(top, folderBounds.bottom - height));
+        }
+        inOutPosition[0] = left;
+        inOutPosition[1] = top;
+    }
+
     /**
      * Unbinds the view for the specified item, and removes the item and all its children.
      *
@@ -1917,6 +1967,13 @@
 
     @Override
     public boolean startActivitySafely(View v, Intent intent, ItemInfo item) {
+        if (isViewInTaskbar(v)) {
+            // Start the activity without the hacky workarounds below, which assume the View was
+            // clicked when Launcher was resumed and will be hidden until Launcher is re-resumed
+            // (this isn't the case for Taskbar).
+            return super.startActivitySafely(v, intent, item);
+        }
+
         if (!hasBeenResumed()) {
             // Workaround an issue where the WM launch animation is clobbered when finishing the
             // recents animation into launcher. Defer launching the activity until Launcher is
@@ -2564,7 +2621,7 @@
     }
 
     @Override
-    public void bindAllWidgets(final ArrayList<WidgetListRowEntry> allWidgets) {
+    public void bindAllWidgets(final List<WidgetsListBaseEntry> allWidgets) {
         mPopupDataProvider.setAllWidgets(allWidgets);
     }
 
@@ -2645,19 +2702,9 @@
             shortcutInfos.add(new KeyboardShortcutInfo(getString(R.string.widget_button_text),
                     KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON));
         }
-        final View currentFocus = getCurrentFocus();
-        if (currentFocus != null) {
-            if (new CustomActionsPopup(this, currentFocus).canShow()) {
-                shortcutInfos.add(new KeyboardShortcutInfo(getString(R.string.custom_actions),
-                        KeyEvent.KEYCODE_O, KeyEvent.META_CTRL_ON));
-            }
-            if (currentFocus.getTag() instanceof ItemInfo
-                    && ShortcutUtil.supportsShortcuts((ItemInfo) currentFocus.getTag())) {
+        getSupportedActions(this,  getCurrentFocus()).forEach(la ->
                 shortcutInfos.add(new KeyboardShortcutInfo(
-                        getString(R.string.shortcuts_menu_with_notifications_description),
-                        KeyEvent.KEYCODE_S, KeyEvent.META_CTRL_ON));
-            }
-        }
+                        la.accessibilityAction.getLabel(), la.keyCode, KeyEvent.META_CTRL_ON)));
         if (!shortcutInfos.isEmpty()) {
             data.add(new KeyboardShortcutGroup(getString(R.string.home_screen), shortcutInfos));
         }
@@ -2675,29 +2722,18 @@
                         return true;
                     }
                     break;
-                case KeyEvent.KEYCODE_S: {
-                    View focusedView = getCurrentFocus();
-                    if (focusedView instanceof BubbleTextView
-                            && focusedView.getTag() instanceof ItemInfo
-                            && mAccessibilityDelegate.performAction(focusedView,
-                            (ItemInfo) focusedView.getTag(),
-                            LauncherAccessibilityDelegate.DEEP_SHORTCUTS)) {
-                        PopupContainerWithArrow.getOpen(this).requestFocus();
-                        return true;
-                    }
-                    break;
-                }
-                case KeyEvent.KEYCODE_O:
-                    if (new CustomActionsPopup(this, getCurrentFocus()).show()) {
-                        return true;
-                    }
-                    break;
                 case KeyEvent.KEYCODE_W:
                     if (isInState(NORMAL)) {
                         OptionsPopupView.openWidgets(this);
                         return true;
                     }
                     break;
+                default:
+                    for (LauncherAction la : getSupportedActions(this, getCurrentFocus())) {
+                        if (la.keyCode == keyCode) {
+                            return la.invokeFromKeyboard(getCurrentFocus());
+                        }
+                    }
             }
         }
         return super.onKeyShortcut(keyCode, event);
@@ -2755,6 +2791,9 @@
         return Stream.of(APP_INFO, WIDGETS, INSTALL);
     }
 
+    protected LauncherAccessibilityDelegate createAccessibilityDelegate() {
+        return new LauncherAccessibilityDelegate(this);
+    }
 
     /**
      * @see LauncherState#getOverviewScaleAndOffset(Launcher)
@@ -2818,8 +2857,24 @@
                 .start();
     }
 
+    /**
+     * @return Whether the View is in the same window as the Taskbar window.
+     */
+    public boolean isViewInTaskbar(View v) {
+        return false;
+    }
+
+    public DragOptions getDefaultWorkspaceDragOptions() {
+        return new DragOptions();
+    }
+
     private static class NonConfigInstance {
         public Configuration config;
         public Bitmap snapshot;
     }
+
+    @Override
+    public StatsLogManager getStatsLogManager() {
+        return super.getStatsLogManager().withDefaultInstanceId(mAllAppsSessionLogId);
+    }
 }
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index a4181c5..57d7600 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -17,8 +17,8 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
+import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
 
 import android.content.ComponentName;
 import android.content.Context;
@@ -37,10 +37,10 @@
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.pm.InstallSessionTracker;
 import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.SafeCloseable;
-import com.android.launcher3.util.SecureSettingsObserver;
 import com.android.launcher3.util.SimpleBroadcastReceiver;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
@@ -57,8 +57,9 @@
     private final IconCache mIconCache;
     private final WidgetPreviewLoader mWidgetCache;
     private final InvariantDeviceProfile mInvariantDeviceProfile;
+    private SettingsCache.OnChangeListener mNotificationSettingsChangedListener;
 
-    private SecureSettingsObserver mNotificationDotsObserver;
+    private SettingsCache mSettingsCache;
     private InstallSessionTracker mInstallSessionTracker;
     private SimpleBroadcastReceiver mModelChangeReceiver;
     private SafeCloseable mCalendarChangeTracker;
@@ -108,10 +109,11 @@
                 .registerInstallTracker(mModel);
 
         // Register an observer to rebind the notification listener when dots are re-enabled.
-        mNotificationDotsObserver =
-                newNotificationSettingsObserver(mContext, this::onNotificationSettingsChanged);
-        mNotificationDotsObserver.register();
-        mNotificationDotsObserver.dispatchOnChange();
+        mSettingsCache = SettingsCache.INSTANCE.get(mContext);
+        mNotificationSettingsChangedListener = this::onNotificationSettingsChanged;
+        mSettingsCache.register(NOTIFICATION_BADGING_URI,
+                mNotificationSettingsChangedListener);
+        mSettingsCache.dispatchOnChange(NOTIFICATION_BADGING_URI);
     }
 
     public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
@@ -166,8 +168,9 @@
         }
         CustomWidgetManager.INSTANCE.get(mContext).setWidgetRefreshCallback(null);
 
-        if (mNotificationDotsObserver != null) {
-            mNotificationDotsObserver.unregister();
+        if (mSettingsCache != null) {
+            mSettingsCache.unregister(NOTIFICATION_BADGING_URI,
+                    mNotificationSettingsChangedListener);
         }
     }
 
diff --git a/src/com/android/launcher3/LauncherAppTransitionManager.java b/src/com/android/launcher3/LauncherAppTransitionManager.java
index ac3ad9f..0fa441a 100644
--- a/src/com/android/launcher3/LauncherAppTransitionManager.java
+++ b/src/com/android/launcher3/LauncherAppTransitionManager.java
@@ -50,7 +50,7 @@
         return ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
     }
 
-    public boolean supportsAdaptiveIconAnimation() {
+    public boolean supportsAdaptiveIconAnimation(View clickedView) {
         return false;
     }
 
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index e89b9b0..699495c 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -615,7 +615,9 @@
                         + "\" bitmapIcon=" + info.bitmap.icon
                         + " componentName=" + info.componentName.getPackageName());
             }
+            writer.println();
         }
+        mModelDelegate.dump(prefix, fd, writer, args);
         mBgDataModel.dump(prefix, fd, writer, args);
     }
 
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index e151777..2ec5e90 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -71,6 +71,7 @@
 import com.android.launcher3.util.NoLocaleSQLiteHelper;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
 
 import org.xmlpull.v1.XmlPullParser;
 
diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java
index 51504ce..76c4518 100644
--- a/src/com/android/launcher3/LauncherRootView.java
+++ b/src/com/android/launcher3/LauncherRootView.java
@@ -4,12 +4,14 @@
 
 import android.annotation.TargetApi;
 import android.content.Context;
+import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.os.Build;
 import android.util.AttributeSet;
 import android.view.ViewDebug;
 import android.view.WindowInsets;
 
+import com.android.launcher3.graphics.SysUiScrim;
 import com.android.launcher3.statemanager.StatefulActivity;
 
 import java.util.Collections;
@@ -31,6 +33,8 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mForceHideBackArrow;
 
+    private SysUiScrim mSysUiScrim;
+
     public LauncherRootView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mActivity = StatefulActivity.fromContext(context);
@@ -89,6 +93,18 @@
         }
     }
 
+    public void setSysUiScrim(SysUiScrim scrim) {
+        mSysUiScrim = scrim;
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        if (mSysUiScrim != null) {
+            mSysUiScrim.draw(canvas);
+        }
+        super.dispatchDraw(canvas);
+    }
+
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         super.onLayout(changed, l, t, r, b);
@@ -119,4 +135,4 @@
 
         void onWindowVisibilityChanged(int visibility);
     }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index fe423ed..22c257a 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -75,6 +75,37 @@
         public static final int ITEM_TYPE_SHORTCUT = 1;
 
         /**
+         * The favorite is a user created folder
+         */
+        public static final int ITEM_TYPE_FOLDER = 2;
+
+        /**
+         * The favorite is a widget
+         */
+        public static final int ITEM_TYPE_APPWIDGET = 4;
+
+        /**
+         * The favorite is a custom widget provided by the launcher
+         */
+        public static final int ITEM_TYPE_CUSTOM_APPWIDGET = 5;
+
+        /**
+         * The gesture is an application created deep shortcut
+         */
+        public static final int ITEM_TYPE_DEEP_SHORTCUT = 6;
+
+        /**
+         * Type of the item is recents task.
+         * TODO(hyunyoungs): move constants not related to Favorites DB to a better location.
+         */
+        public static final int ITEM_TYPE_TASK = 7;
+
+        /**
+         * The item is QSB
+         */
+        public static final int ITEM_TYPE_QSB = 8;
+
+        /**
          * The icon package name in Intent.ShortcutIconResource
          * <P>Type: TEXT</P>
          */
@@ -162,6 +193,7 @@
         public static final int CONTAINER_DESKTOP = -100;
         public static final int CONTAINER_HOTSEAT = -101;
         public static final int CONTAINER_PREDICTION = -102;
+        public static final int CONTAINER_WIDGETS_PREDICTION = -111;
         public static final int CONTAINER_HOTSEAT_PREDICTION = -103;
         public static final int CONTAINER_ALL_APPS = -104;
         public static final int CONTAINER_WIDGETS_TRAY = -105;
@@ -170,6 +202,10 @@
         public static final int CONTAINER_SHORTCUTS = -107;
         public static final int CONTAINER_SETTINGS = -108;
         public static final int CONTAINER_TASKSWITCHER = -109;
+        public static final int CONTAINER_QSB = -110;
+
+        // Represents any of the extended containers implemented in non-AOSP variants.
+        public static final int EXTENDED_CONTAINERS = -200;
 
         public static final String containerToString(int container) {
             switch (container) {
@@ -192,6 +228,8 @@
                 case ITEM_TYPE_APPWIDGET: return "WIDGET";
                 case ITEM_TYPE_CUSTOM_APPWIDGET: return "CUSTOMWIDGET";
                 case ITEM_TYPE_DEEP_SHORTCUT: return "DEEPSHORTCUT";
+                case ITEM_TYPE_TASK: return "TASK";
+                case ITEM_TYPE_QSB: return "QSB";
                 default: return String.valueOf(type);
             }
         }
@@ -237,32 +275,6 @@
         public static final String PROFILE_ID = "profileId";
 
         /**
-         * The favorite is a user created folder
-         */
-        public static final int ITEM_TYPE_FOLDER = 2;
-
-        /**
-         * The favorite is a widget
-         */
-        public static final int ITEM_TYPE_APPWIDGET = 4;
-
-        /**
-         * The favorite is a custom widget provided by the launcher
-         */
-        public static final int ITEM_TYPE_CUSTOM_APPWIDGET = 5;
-
-        /**
-         * The gesture is an application created deep shortcut
-         */
-        public static final int ITEM_TYPE_DEEP_SHORTCUT = 6;
-
-        /**
-         * Type of the item is recents task.
-         * TODO(hyunyoungs): move constants not related to Favorites DB to a better location.
-         */
-        public static final int ITEM_TYPE_TASK = 7;
-
-        /**
          * The appWidgetId of the widget
          *
          * <P>Type: INTEGER</P>
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 79476fc..21c40ef 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -56,7 +56,10 @@
     public static final int ALL_APPS_HEADER_EXTRA = 1 << 3; // e.g. app predictions
     public static final int ALL_APPS_CONTENT = 1 << 4;
     public static final int VERTICAL_SWIPE_INDICATOR = 1 << 5;
-    public static final int OVERVIEW_BUTTONS = 1 << 6;
+    public static final int OVERVIEW_ACTIONS = 1 << 6;
+    public static final int TASKBAR = 1 << 7;
+    public static final int CLEAR_ALL_BUTTON = 1 << 8;
+    public static final int WORKSPACE_PAGE_INDICATOR = 1 << 9;
 
     /** Mask of all the items that are contained in the apps view. */
     public static final int APPS_VIEW_ITEM_MASK =
@@ -186,15 +189,26 @@
     }
 
     public int getVisibleElements(Launcher launcher) {
-        int flags = HOTSEAT_ICONS | VERTICAL_SWIPE_INDICATOR;
-        if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get()
-                && !launcher.getDeviceProfile().isVerticalBarLayout()) {
+        DeviceProfile deviceProfile = launcher.getDeviceProfile();
+        int flags = WORKSPACE_PAGE_INDICATOR | VERTICAL_SWIPE_INDICATOR | TASKBAR;
+        if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get() && !deviceProfile.isVerticalBarLayout()) {
             flags |= HOTSEAT_SEARCH_BOX;
         }
+        if (!deviceProfile.isTaskbarPresent) {
+            flags |= HOTSEAT_ICONS;
+        }
         return flags;
     }
 
     /**
+     * A shorthand for checking getVisibleElements() & elements == elements.
+     * @return Whether all of the given elements are visible.
+     */
+    public boolean areElementsVisible(Launcher launcher, int elements) {
+        return (getVisibleElements(launcher) & elements) == elements;
+    }
+
+    /**
      * Fraction shift in the vertical translation UI and related properties
      *
      * @see com.android.launcher3.allapps.AllAppsTransitionController
@@ -220,6 +234,13 @@
     }
 
     /**
+     * For this state, whether tasks should layout as a grid rather than a list.
+     */
+    public boolean displayOverviewTasksAsGrid(Launcher launcher) {
+        return false;
+    }
+
+    /**
      * The amount of blur and wallpaper zoom to apply to the background of either the app
      * or Launcher surface in this state. Should be a number between 0 and 1, inclusive.
      *
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 4303dee..50f1e44 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -217,7 +217,7 @@
     /**
      * Returns the index of the currently displayed page. When in free scroll mode, this is the page
      * that the user was on before entering free scroll mode (e.g. the home screen page they
-     * long-pressed on to enter the overview). Try using {@link #getPageNearestToCenterOfScreen()}
+     * long-pressed on to enter the overview). Try using {@link #getDestinationPage()}
      * to get the page the user is currently scrolling over.
      */
     public int getCurrentPage() {
@@ -745,6 +745,11 @@
         return mOrientationHandler.getChildStart(pageAtIndex);
     }
 
+    protected int getChildVisibleSize(int index) {
+        View layout = getPageAt(index);
+        return mOrientationHandler.getMeasuredSize(layout);
+    }
+
     @Override
     public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
         int page = indexToPage(indexOfChild(child));
@@ -1272,7 +1277,7 @@
                     if (((initialScroll >= maxScroll) && (isVelocityLeft || !isFling)) ||
                         ((initialScroll <= minScroll) && (!isVelocityLeft || !isFling))) {
                         mScroller.springBack(initialScroll, minScroll, maxScroll);
-                        mNextPage = getPageNearestToCenterOfScreen();
+                        mNextPage = getDestinationPage();
                     } else {
                         mScroller.setInterpolator(mDefaultInterpolator);
                         mScroller.fling(initialScroll, -velocity,
@@ -1280,11 +1285,12 @@
                             Math.round(getWidth() * 0.5f * OVERSCROLL_DAMP_FACTOR));
 
                         int finalPos = mScroller.getFinalPos();
-                        mNextPage = getPageNearestToCenterOfScreen(finalPos);
+                        mNextPage = getDestinationPage(finalPos);
 
                         int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1);
                         int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0);
-                        if (finalPos > minScroll && finalPos < maxScroll) {
+                        if (snapToPageInFreeScroll() && finalPos > minScroll
+                                && finalPos < maxScroll) {
                             // If scrolling ends in the half of the added space that is closer to
                             // the end, settle to the end. Otherwise snap to the nearest page.
                             // If flinging past one of the ends, don't change the velocity as it
@@ -1330,6 +1336,10 @@
         return true;
     }
 
+    protected boolean snapToPageInFreeScroll() {
+        return true;
+    }
+
     protected boolean shouldFlingForVelocity(int velocity) {
         float threshold = mAllowEasyFling ? mEasyFlingThresholdVelocity : mFlingThresholdVelocity;
         return Math.abs(velocity) > threshold;
@@ -1435,6 +1445,14 @@
         }
     }
 
+    public int getDestinationPage() {
+        return getDestinationPage(mOrientationHandler.getPrimaryScroll(this));
+    }
+
+    protected int getDestinationPage(int scaledScroll) {
+        return getPageNearestToCenterOfScreen(scaledScroll);
+    }
+
     public int getPageNearestToCenterOfScreen() {
         return getPageNearestToCenterOfScreen(mOrientationHandler.getPrimaryScroll(this));
     }
@@ -1457,8 +1475,7 @@
     }
 
     private int getDisplacementFromScreenCenter(int childIndex, int screenCenter) {
-        View layout = getPageAt(childIndex);
-        int childSize = mOrientationHandler.getMeasuredSize(layout);
+        int childSize = getChildVisibleSize(childIndex);
         int halfChildSize = (childSize / 2);
         int childCenter = getChildOffset(childIndex) + halfChildSize;
         return childCenter - screenCenter;
@@ -1471,7 +1488,7 @@
     }
 
     protected void snapToDestination() {
-        snapToPage(getPageNearestToCenterOfScreen(), getPageSnapDuration());
+        snapToPage(getDestinationPage(), getPageSnapDuration());
     }
 
     protected boolean isInOverScroll() {
diff --git a/src/com/android/launcher3/ResourceUtils.java b/src/com/android/launcher3/ResourceUtils.java
index f60e1f8..ece123d 100644
--- a/src/com/android/launcher3/ResourceUtils.java
+++ b/src/com/android/launcher3/ResourceUtils.java
@@ -60,7 +60,11 @@
     }
 
     public static int pxFromDp(float size, DisplayMetrics metrics) {
-        return size < 0 ? INVALID_RESOURCE_HANDLE : Math.round(
-                TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, metrics));
+        return pxFromDp(size, metrics, 1f);
+    }
+
+    public static int pxFromDp(float size, DisplayMetrics metrics, float scale) {
+        return size < 0 ? INVALID_RESOURCE_HANDLE : Math.round(scale
+                * TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, metrics));
     }
 }
diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java
index 92b88e6..4fd87cb 100644
--- a/src/com/android/launcher3/SecondaryDropTarget.java
+++ b/src/com/android/launcher3/SecondaryDropTarget.java
@@ -37,6 +37,8 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.logging.InstanceId;
+import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.model.data.ItemInfo;
@@ -44,6 +46,7 @@
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Themes;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 import java.net.URISyntaxException;
 
@@ -203,9 +206,13 @@
         d.dragSource = new DeferredOnComplete(d.dragSource, getContext());
 
         super.onDrop(d, options);
-        StatsLogger logger = mStatsLogManager.logger().withInstanceId(d.logInstanceId);
-        if (d.originalDragInfo != null) {
-            logger.withItemInfo(d.originalDragInfo);
+        doLog(d.logInstanceId, d.originalDragInfo);
+    }
+
+    private void doLog(InstanceId logInstanceId, ItemInfo itemInfo) {
+        StatsLogger logger = mStatsLogManager.logger().withInstanceId(logInstanceId);
+        if (itemInfo != null) {
+            logger.withItemInfo(itemInfo);
         }
         if (mCurrentAccessibilityAction == UNINSTALL) {
             logger.log(LAUNCHER_ITEM_DROPPED_ON_UNINSTALL);
@@ -296,6 +303,7 @@
 
     @Override
     public void onAccessibilityDrop(View view, ItemInfo item) {
+        doLog(new InstanceIdSequence().newInstanceId(), item);
         performDropAction(view, item);
     }
 
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index ee0c7bb..2c24c3a 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -26,24 +26,29 @@
 import android.view.ViewGroup;
 
 import com.android.launcher3.CellLayout.ContainerType;
+import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 
-public class ShortcutAndWidgetContainer extends ViewGroup {
+public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon.FolderIconParent {
     static final String TAG = "ShortcutAndWidgetContainer";
 
     // These are temporary variables to prevent having to allocate a new object just to
     // return an (x, y) value from helper functions. Do NOT use them to maintain other state.
     private final int[] mTmpCellXY = new int[2];
 
+    private final Rect mTempRect = new Rect();
+
     @ContainerType
     private final int mContainerType;
     private final WallpaperManager mWallpaperManager;
 
     private int mCellWidth;
     private int mCellHeight;
+    private int mBorderSpacing;
 
     private int mCountX;
+    private int mCountY;
 
     private final ActivityContext mActivity;
     private boolean mInvertIfRtl = false;
@@ -55,20 +60,23 @@
         mContainerType = containerType;
     }
 
-    public void setCellDimensions(int cellWidth, int cellHeight, int countX, int countY) {
+    public void setCellDimensions(int cellWidth, int cellHeight, int countX, int countY,
+            int borderSpacing) {
         mCellWidth = cellWidth;
         mCellHeight = cellHeight;
         mCountX = countX;
+        mCountY = countY;
+        mBorderSpacing = borderSpacing;
     }
 
-    public View getChildAt(int x, int y) {
+    public View getChildAt(int cellX, int cellY) {
         final int count = getChildCount();
         for (int i = 0; i < count; i++) {
             View child = getChildAt(i);
             CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
 
-            if ((lp.cellX <= x) && (x < lp.cellX + lp.cellHSpan) &&
-                    (lp.cellY <= y) && (y < lp.cellY + lp.cellVSpan)) {
+            if ((lp.cellX <= cellX) && (cellX < lp.cellX + lp.cellHSpan)
+                    && (lp.cellY <= cellY) && (cellY < lp.cellY + lp.cellVSpan)) {
                 return child;
             }
         }
@@ -95,10 +103,12 @@
         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
         if (child instanceof LauncherAppWidgetHostView) {
             DeviceProfile profile = mActivity.getDeviceProfile();
-            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX,
-                    profile.appWidgetScale.x, profile.appWidgetScale.y);
+            ((LauncherAppWidgetHostView) child).getWidgetInset(profile, mTempRect);
+            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
+                    profile.appWidgetScale.x, profile.appWidgetScale.y, mBorderSpacing, mTempRect);
         } else {
-            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX);
+            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
+                    mBorderSpacing, null);
         }
     }
 
@@ -117,11 +127,12 @@
         final DeviceProfile profile = mActivity.getDeviceProfile();
 
         if (child instanceof LauncherAppWidgetHostView) {
-            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX,
-                    profile.appWidgetScale.x, profile.appWidgetScale.y);
-            // Widgets have their own padding
+            ((LauncherAppWidgetHostView) child).getWidgetInset(profile, mTempRect);
+            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
+                    profile.appWidgetScale.x, profile.appWidgetScale.y, mBorderSpacing, mTempRect);
         } else {
-            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX);
+            lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
+                    mBorderSpacing, null);
             // Center the icon/folder
             int cHeight = getCellContentHeight();
             int cellPaddingY = (int) Math.max(0, ((lp.height - cHeight) / 2f));
@@ -221,4 +232,24 @@
             child.cancelLongPress();
         }
     }
+
+    @Override
+    public void drawFolderLeaveBehindForIcon(FolderIcon child) {
+        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
+        // While the folder is open, the position of the icon cannot change.
+        lp.canReorder = false;
+        if (mContainerType == CellLayout.HOTSEAT) {
+            CellLayout cl = (CellLayout) getParent();
+            cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY);
+        }
+    }
+
+    @Override
+    public void clearFolderLeaveBehind(FolderIcon child) {
+        ((CellLayout.LayoutParams) child.getLayoutParams()).canReorder = true;
+        if (mContainerType == CellLayout.HOTSEAT) {
+            CellLayout cl = (CellLayout) getParent();
+            cl.clearFolderLeaveBehind();
+        }
+    }
 }
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 51d8e66..c440303 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -605,7 +605,6 @@
                 outObj[0] = activityInfo;
                 return activityInfo.getFullResIcon(appState.getIconCache());
             }
-            if (info.getIntent() == null || info.getIntent().getPackage() == null) return null;
             List<ShortcutInfo> si = ShortcutKey.fromItemInfo(info)
                     .buildRequest(launcher)
                     .query(ShortcutRequest.ALL);
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java
index 2c45c77..446c873 100644
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ b/src/com/android/launcher3/WidgetPreviewLoader.java
@@ -50,6 +50,7 @@
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.SQLiteCacheHelper;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetCell;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 8a45c81..87fb6fb 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -63,7 +63,6 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.Toast;
 
-import com.android.launcher3.LauncherAppWidgetHost.ProviderChangedListener;
 import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
 import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
 import com.android.launcher3.anim.Interpolators;
@@ -81,6 +80,7 @@
 import com.android.launcher3.folder.PreviewBackground;
 import com.android.launcher3.graphics.DragPreviewProvider;
 import com.android.launcher3.graphics.PreloadIconDrawable;
+import com.android.launcher3.graphics.WorkspaceDragScrim;
 import com.android.launcher3.icons.BitmapRenderer;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logging.StatsLogManager;
@@ -103,6 +103,8 @@
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.util.WallpaperOffsetInterpolator;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
+import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListener;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
@@ -199,6 +201,9 @@
     private boolean mStripScreensOnPageStopMoving = false;
 
     private DragPreviewProvider mOutlineProvider = null;
+
+    private WorkspaceDragScrim mWorkspaceDragScrim;
+
     private boolean mWorkspaceFadeInAdjacentScreens;
 
     final WallpaperOffsetInterpolator mWallpaperOffset;
@@ -412,7 +417,7 @@
                 // widgets as they cannot be placed inside a folder.
                 // Start at the current page and search right (on LTR) until finding a page with
                 // enough space. Since an empty screen is the furthest right, a page must be found.
-                int currentPage = getPageNearestToCenterOfScreen();
+                int currentPage = getDestinationPage();
                 for (int pageIndex = currentPage; pageIndex < getPageCount(); pageIndex++) {
                     CellLayout page = (CellLayout) getPageAt(pageIndex);
                     if (page.hasReorderSolution(dragObject.dragInfo)) {
@@ -441,6 +446,19 @@
         }
 
         updateChildrenLayersEnabled();
+        StateManager<LauncherState> stateManager = mLauncher.getStateManager();
+        stateManager.addStateListener(new StateManager.StateListener<LauncherState>() {
+            @Override
+            public void onStateTransitionComplete(LauncherState finalState) {
+                if (finalState == NORMAL) {
+                    if (!mDeferRemoveExtraEmptyScreen) {
+                        removeExtraEmptyScreen(true /* stripEmptyScreens */);
+                    }
+                    stateManager.removeStateListener(this);
+                }
+            }
+        });
+
         mDragInfo = null;
         mOutlineProvider = null;
         mDragSourceInternal = null;
@@ -1161,6 +1179,19 @@
         }
     }
 
+    public void setWorkspaceDragScrim(WorkspaceDragScrim workspaceDragScrim) {
+        mWorkspaceDragScrim = workspaceDragScrim;
+    }
+
+    @Override
+    public void invalidate() {
+        // The workspace scrim may need to be re-rendered based on the workspace scroll
+        if (mWorkspaceDragScrim != null) {
+            mWorkspaceDragScrim.invalidate();
+        }
+        super.invalidate();
+    }
+
     @Override
     public void computeScroll() {
         super.computeScroll();
@@ -1706,10 +1737,9 @@
         if (dropOverView instanceof FolderIcon) {
             FolderIcon fi = (FolderIcon) dropOverView;
             if (fi.acceptDrop(d.dragInfo)) {
-                mStatsLogManager.logger().withItemInfo(d.dragInfo).withInstanceId(d.logInstanceId)
-                        .log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED);
+                mStatsLogManager.logger().withItemInfo(fi.mInfo).withInstanceId(d.logInstanceId)
+                        .log(LauncherEvent.LAUNCHER_ITEM_DROP_COMPLETED_ON_FOLDER_ICON);
                 fi.onDrop(d, false /* itemReturnedOnFailedDrop */);
-
                 // if the drag started here, we need to remove it from the workspace
                 if (!external) {
                     getParentCellLayoutForView(mDragInfo.cell).removeView(mDragInfo.cell);
@@ -1860,19 +1890,6 @@
                             };
                         }
                     }
-                    StateManager<LauncherState> stateManager = mLauncher.getStateManager();
-                    stateManager.addStateListener(new StateManager.StateListener<LauncherState>() {
-                        @Override
-                        public void onStateTransitionComplete(LauncherState finalState) {
-                            if (finalState == NORMAL) {
-                                if (!mDeferRemoveExtraEmptyScreen) {
-                                    removeExtraEmptyScreen(true /* stripEmptyScreens */);
-                                }
-                                stateManager.removeStateListener(this);
-                            }
-                        }
-                    });
-
                     mLauncher.getModelWriter().modifyItemInDatabase(info, container, screenId,
                             lp.cellX, lp.cellY, item.spanX, item.spanY);
                 } else {
@@ -2028,7 +2045,7 @@
         }
         // Invalidating the scrim will also force this CellLayout
         // to be invalidated so that it is highlighted if necessary.
-        mLauncher.getDragLayer().getScrim().invalidate();
+        mLauncher.getDragLayer().getWorkspaceDragScrim().invalidate();
     }
 
     public CellLayout getCurrentDragOverlappingLayout() {
@@ -2956,7 +2973,7 @@
      * @param operators List of operators, in order starting from best matching operator.
      * @return
      */
-    private View getFirstMatch(CellLayout[] cellLayouts, final ItemOperator... operators) {
+    View getFirstMatch(CellLayout[] cellLayouts, final ItemOperator... operators) {
         // This array is filled with the first match for each operator.
         final View[] matches = new View[operators.length];
         // For efficiency, the outer loop should be CellLayout.
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index cd938e1..660eeab 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -28,11 +28,13 @@
 import static com.android.launcher3.LauncherState.HINT_STATE;
 import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.WORKSPACE_PAGE_INDICATOR;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.ZOOM_OUT;
 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
 import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS;
-import static com.android.launcher3.graphics.WorkspaceAndHotseatScrim.SYSUI_PROGRESS;
+import static com.android.launcher3.graphics.SysUiScrim.SYSUI_PROGRESS;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_TRANSLATE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
@@ -49,7 +51,8 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.anim.SpringAnimationBuilder;
-import com.android.launcher3.graphics.WorkspaceAndHotseatScrim;
+import com.android.launcher3.graphics.SysUiScrim;
+import com.android.launcher3.graphics.WorkspaceDragScrim;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.util.DynamicResource;
 import com.android.systemui.plugins.ResourceProvider;
@@ -140,9 +143,11 @@
             }
 
             float hotseatIconsAlpha = (elements & HOTSEAT_ICONS) != 0 ? 1 : 0;
-            propertySetter.setViewAlpha(hotseat, hotseatIconsAlpha, fadeInterpolator);
+            propertySetter.setViewAlpha(hotseat, hotseatIconsAlpha,
+                    config.getInterpolator(ANIM_HOTSEAT_FADE, fadeInterpolator));
+            float workspacePageIndicatorAlpha = (elements & WORKSPACE_PAGE_INDICATOR) != 0 ? 1 : 0;
             propertySetter.setViewAlpha(mLauncher.getWorkspace().getPageIndicator(),
-                    hotseatIconsAlpha, fadeInterpolator);
+                    workspacePageIndicatorAlpha, fadeInterpolator);
         }
 
         if (config.onlyPlayAtomicComponent()) {
@@ -183,10 +188,12 @@
     }
 
     public void setScrim(PropertySetter propertySetter, LauncherState state) {
-        WorkspaceAndHotseatScrim scrim = mLauncher.getDragLayer().getScrim();
-        propertySetter.setFloat(scrim, SCRIM_PROGRESS, state.getWorkspaceScrimAlpha(mLauncher),
-                LINEAR);
-        propertySetter.setFloat(scrim, SYSUI_PROGRESS,
+        WorkspaceDragScrim workspaceDragScrim = mLauncher.getDragLayer().getWorkspaceDragScrim();
+        propertySetter.setFloat(workspaceDragScrim, SCRIM_PROGRESS,
+                state.getWorkspaceScrimAlpha(mLauncher), LINEAR);
+
+        SysUiScrim sysUiScrim = mLauncher.getDragLayer().getSysUiScrim();
+        propertySetter.setFloat(sysUiScrim, SYSUI_PROGRESS,
                 state.hasFlag(FLAG_HAS_SYS_UI_SCRIM) ? 1 : 0, LINEAR);
     }
 
diff --git a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
index ddb547f..d0fc175 100644
--- a/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/DragAndDropAccessibilityDelegate.java
@@ -31,6 +31,7 @@
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
+import com.android.launcher3.dragndrop.DragLayer;
 
 import java.util.List;
 
@@ -41,30 +42,32 @@
         implements OnClickListener, OnHoverListener {
     protected static final int INVALID_POSITION = -1;
 
-    private static final int[] sTempArray = new int[2];
+    protected final Rect mTempRect = new Rect();
+    protected final int[] mTempCords = new int[2];
 
     protected final CellLayout mView;
     protected final Context mContext;
     protected final LauncherAccessibilityDelegate mDelegate;
-
-    private final Rect mTempRect = new Rect();
+    protected final DragLayer mDragLayer;
 
     public DragAndDropAccessibilityDelegate(CellLayout forView) {
         super(forView);
         mView = forView;
         mContext = mView.getContext();
-        mDelegate = Launcher.getLauncher(mContext).getAccessibilityDelegate();
+        Launcher launcher = Launcher.getLauncher(mContext);
+        mDelegate = launcher.getAccessibilityDelegate();
+        mDragLayer = launcher.getDragLayer();
     }
 
     @Override
-    protected int getVirtualViewAt(float x, float y) {
+    public int getVirtualViewAt(float x, float y) {
         if (x < 0 || y < 0 || x > mView.getMeasuredWidth() || y > mView.getMeasuredHeight()) {
             return INVALID_ID;
         }
-        mView.pointToCellExact((int) x, (int) y, sTempArray);
+        mView.pointToCellExact((int) x, (int) y, mTempCords);
 
         // Map cell to id
-        int id = sTempArray[0] + sTempArray[1] * mView.getCountX();
+        int id = mTempCords[0] + mTempCords[1] * mView.getCountX();
         return intersectsValidDropTarget(id);
     }
 
@@ -75,7 +78,7 @@
     protected abstract int intersectsValidDropTarget(int id);
 
     @Override
-    protected void getVisibleVirtualViews(List<Integer> virtualViews) {
+    public void getVisibleVirtualViews(List<Integer> virtualViews) {
         // We create a virtual view for each cell of the grid
         // The cell ids correspond to cells in reading order.
         int nCells = mView.getCountX() * mView.getCountY();
@@ -88,7 +91,7 @@
     }
 
     @Override
-    protected boolean onPerformActionForVirtualView(int viewId, int action, Bundle args) {
+    public boolean onPerformActionForVirtualView(int viewId, int action, Bundle args) {
         if (action == AccessibilityNodeInfoCompat.ACTION_CLICK && viewId != INVALID_ID) {
             String confirmation = getConfirmationForIconDrop(viewId);
             mDelegate.handleAccessibleDrop(mView, getItemBounds(viewId), confirmation);
@@ -112,13 +115,25 @@
     }
 
     @Override
-    protected void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) {
+    public void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) {
         if (id == INVALID_ID) {
             throw new IllegalArgumentException("Invalid virtual view id");
         }
 
         node.setContentDescription(getLocationDescriptionForIconDrop(id));
-        node.setBoundsInParent(getItemBounds(id));
+
+        Rect itemBounds = getItemBounds(id);
+        node.setBoundsInParent(itemBounds);
+
+        // ExploreByTouchHelper does not currently handle view scale.
+        // Update BoundsInScreen to appropriate value.
+        mTempCords[0] = mTempCords[1] = 0;
+        float scale = mDragLayer.getDescendantCoordRelativeToSelf(mView, mTempCords);
+        mTempRect.left = mTempCords[0] + (int) (itemBounds.left * scale);
+        mTempRect.right = mTempCords[0] + (int) (itemBounds.right * scale);
+        mTempRect.top = mTempCords[1] + (int) (itemBounds.top * scale);
+        mTempRect.bottom = mTempCords[1] + (int) (itemBounds.bottom * scale);
+        node.setBoundsInScreen(mTempRect);
 
         node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
         node.setClickable(true);
@@ -130,6 +145,13 @@
         return dispatchHoverEvent(motionEvent);
     }
 
+    /**
+     * Returns the target host container
+     */
+    public View getHost() {
+        return mView;
+    }
+
     protected abstract String getLocationDescriptionForIconDrop(int id);
 
     protected abstract String getConfirmationForIconDrop(int id);
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 136d43e..db7fd3f 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -3,17 +3,18 @@
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
 
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
 
-import android.app.AlertDialog;
 import android.appwidget.AppWidgetProviderInfo;
-import android.content.DialogInterface;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.RectF;
 import android.os.Bundle;
 import android.os.Handler;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
+import android.view.KeyEvent;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -25,7 +26,6 @@
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.PendingAddItemInfo;
 import com.android.launcher3.R;
@@ -33,21 +33,26 @@
 import com.android.launcher3.dragndrop.DragController.DragListener;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.folder.Folder;
-import com.android.launcher3.keyboard.CustomActionsPopup;
+import com.android.launcher3.keyboard.KeyboardDragAndDropView;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.notification.NotificationListener;
+import com.android.launcher3.popup.ArrowPopup;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.ShortcutUtil;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.views.OptionsPopupView;
+import com.android.launcher3.views.OptionsPopupView.OptionItem;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 public class LauncherAccessibilityDelegate extends AccessibilityDelegate implements DragListener {
 
@@ -77,93 +82,105 @@
         public View item;
     }
 
-    protected final SparseArray<AccessibilityAction> mActions = new SparseArray<>();
-    @Thunk final Launcher mLauncher;
+    protected final SparseArray<LauncherAction> mActions = new SparseArray<>();
+    protected final Launcher mLauncher;
 
     private DragInfo mDragInfo = null;
 
     public LauncherAccessibilityDelegate(Launcher launcher) {
         mLauncher = launcher;
 
-        mActions.put(REMOVE, new AccessibilityAction(REMOVE,
-                launcher.getText(R.string.remove_drop_target_label)));
-        mActions.put(UNINSTALL, new AccessibilityAction(UNINSTALL,
-                launcher.getText(R.string.uninstall_drop_target_label)));
-        mActions.put(DISMISS_PREDICTION, new AccessibilityAction(DISMISS_PREDICTION,
-                launcher.getText(R.string.dismiss_prediction_label)));
-        mActions.put(RECONFIGURE, new AccessibilityAction(RECONFIGURE,
-                launcher.getText(R.string.gadget_setup_text)));
-        mActions.put(ADD_TO_WORKSPACE, new AccessibilityAction(ADD_TO_WORKSPACE,
-                launcher.getText(R.string.action_add_to_workspace)));
-        mActions.put(MOVE, new AccessibilityAction(MOVE,
-                launcher.getText(R.string.action_move)));
-        mActions.put(MOVE_TO_WORKSPACE, new AccessibilityAction(MOVE_TO_WORKSPACE,
-                launcher.getText(R.string.action_move_to_workspace)));
-        mActions.put(RESIZE, new AccessibilityAction(RESIZE,
-                        launcher.getText(R.string.action_resize)));
-        mActions.put(DEEP_SHORTCUTS, new AccessibilityAction(DEEP_SHORTCUTS,
-                launcher.getText(R.string.action_deep_shortcut)));
-        mActions.put(SHORTCUTS_AND_NOTIFICATIONS, new AccessibilityAction(DEEP_SHORTCUTS,
-                launcher.getText(R.string.shortcuts_menu_with_notifications_description)));
-    }
-
-    public void addAccessibilityAction(int action, int actionLabel) {
-        mActions.put(action, new AccessibilityAction(action, mLauncher.getText(actionLabel)));
+        mActions.put(REMOVE, new LauncherAction(
+                REMOVE, R.string.remove_drop_target_label, KeyEvent.KEYCODE_X));
+        mActions.put(UNINSTALL, new LauncherAction(
+                UNINSTALL, R.string.uninstall_drop_target_label, KeyEvent.KEYCODE_U));
+        mActions.put(DISMISS_PREDICTION, new LauncherAction(DISMISS_PREDICTION,
+                R.string.dismiss_prediction_label, KeyEvent.KEYCODE_X));
+        mActions.put(RECONFIGURE, new LauncherAction(
+                RECONFIGURE, R.string.gadget_setup_text, KeyEvent.KEYCODE_E));
+        mActions.put(ADD_TO_WORKSPACE, new LauncherAction(
+                ADD_TO_WORKSPACE, R.string.action_add_to_workspace, KeyEvent.KEYCODE_P));
+        mActions.put(MOVE, new LauncherAction(
+                MOVE, R.string.action_move, KeyEvent.KEYCODE_M));
+        mActions.put(MOVE_TO_WORKSPACE, new LauncherAction(MOVE_TO_WORKSPACE,
+                R.string.action_move_to_workspace, KeyEvent.KEYCODE_P));
+        mActions.put(RESIZE, new LauncherAction(
+                RESIZE, R.string.action_resize, KeyEvent.KEYCODE_R));
+        mActions.put(DEEP_SHORTCUTS, new LauncherAction(DEEP_SHORTCUTS,
+                R.string.action_deep_shortcut, KeyEvent.KEYCODE_S));
+        mActions.put(SHORTCUTS_AND_NOTIFICATIONS, new LauncherAction(DEEP_SHORTCUTS,
+                R.string.shortcuts_menu_with_notifications_description, KeyEvent.KEYCODE_S));
     }
 
     @Override
     public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(host, info);
-        addSupportedActions(host, info, false);
+        if (host.getTag() instanceof ItemInfo) {
+            ItemInfo item = (ItemInfo) host.getTag();
+
+            List<LauncherAction> actions = new ArrayList<>();
+            getSupportedActions(host, item, actions);
+            actions.forEach(la -> info.addAction(la.accessibilityAction));
+
+            if (!itemSupportsLongClick(host, item)) {
+                info.setLongClickable(false);
+                info.removeAction(AccessibilityAction.ACTION_LONG_CLICK);
+            }
+        }
     }
 
-    public void addSupportedActions(View host, AccessibilityNodeInfo info, boolean fromKeyboard) {
-        if (!(host.getTag() instanceof ItemInfo)) return;
-        ItemInfo item = (ItemInfo) host.getTag();
-
-        if (host instanceof AccessibilityActionHandler) {
-            ((AccessibilityActionHandler) host).addSupportedAccessibilityActions(info);
-        }
-
+    /**
+     * Adds all the accessibility actions that can be handled.
+     */
+    protected void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out) {
         // If the request came from keyboard, do not add custom shortcuts as that is already
         // exposed as a direct shortcut
-        if (!fromKeyboard && ShortcutUtil.supportsShortcuts(item)) {
-            info.addAction(mActions.get(NotificationListener.getInstanceIfConnected() != null
+        if (ShortcutUtil.supportsShortcuts(item)) {
+            out.add(mActions.get(NotificationListener.getInstanceIfConnected() != null
                     ? SHORTCUTS_AND_NOTIFICATIONS : DEEP_SHORTCUTS));
         }
 
         for (ButtonDropTarget target : mLauncher.getDropTargetBar().getDropTargets()) {
             if (target.supportsAccessibilityDrop(item, host)) {
-                info.addAction(mActions.get(target.getAccessibilityAction()));
+                out.add(mActions.get(target.getAccessibilityAction()));
             }
         }
 
         // Do not add move actions for keyboard request as this uses virtual nodes.
-        if (!fromKeyboard && itemSupportsAccessibleDrag(item)) {
-            info.addAction(mActions.get(MOVE));
+        if (itemSupportsAccessibleDrag(item)) {
+            out.add(mActions.get(MOVE));
 
             if (item.container >= 0) {
-                info.addAction(mActions.get(MOVE_TO_WORKSPACE));
+                out.add(mActions.get(MOVE_TO_WORKSPACE));
             } else if (item instanceof LauncherAppWidgetInfo) {
                 if (!getSupportedResizeActions(host, (LauncherAppWidgetInfo) item).isEmpty()) {
-                    info.addAction(mActions.get(RESIZE));
+                    out.add(mActions.get(RESIZE));
                 }
             }
         }
 
-        if (!fromKeyboard && !itemSupportsLongClick(host, item)) {
-            info.setLongClickable(false);
-            info.removeAction(AccessibilityAction.ACTION_LONG_CLICK);
-        }
-
         if ((item instanceof AppInfo) || (item instanceof PendingAddItemInfo)) {
-            info.addAction(mActions.get(ADD_TO_WORKSPACE));
+            out.add(mActions.get(ADD_TO_WORKSPACE));
         }
     }
 
+    /**
+     * Returns all the accessibility actions that can be handled by the host.
+     */
+    public static List<LauncherAction> getSupportedActions(Launcher launcher, View host) {
+        if (host == null || !(host.getTag() instanceof  ItemInfo)) {
+            return Collections.emptyList();
+        }
+        PopupContainerWithArrow container = PopupContainerWithArrow.getOpen(launcher);
+        LauncherAccessibilityDelegate delegate = container != null
+                ? container.getAccessibilityDelegate() : launcher.getAccessibilityDelegate();
+        List<LauncherAction> result = new ArrayList<>();
+        delegate.getSupportedActions(host, (ItemInfo) host.getTag(), result);
+        return result;
+    }
+
     private boolean itemSupportsLongClick(View host, ItemInfo info) {
-        return PopupContainerWithArrow.canShow(host, info)
-                || new CustomActionsPopup(mLauncher, host).canShow();
+        return PopupContainerWithArrow.canShow(host, info);
     }
 
     private boolean itemSupportsAccessibleDrag(ItemInfo item) {
@@ -178,13 +195,17 @@
     @Override
     public boolean performAccessibilityAction(View host, int action, Bundle args) {
         if ((host.getTag() instanceof ItemInfo)
-                && performAction(host, (ItemInfo) host.getTag(), action)) {
+                && performAction(host, (ItemInfo) host.getTag(), action, false)) {
             return true;
         }
         return super.performAccessibilityAction(host, action, args);
     }
 
-    public boolean performAction(final View host, final ItemInfo item, int action) {
+    /**
+     * Performs the provided action on the host
+     */
+    protected boolean performAction(final View host, final ItemInfo item, int action,
+            boolean fromKeyboard) {
         if (action == ACTION_LONG_CLICK) {
             if (PopupContainerWithArrow.canShow(host, item)) {
                 // Long press should be consumed for workspace items, and it should invoke the
@@ -192,20 +213,9 @@
                 // standard long press path does.
                 PopupContainerWithArrow.showForIcon((BubbleTextView) host);
                 return true;
-            } else {
-                CustomActionsPopup popup = new CustomActionsPopup(mLauncher, host);
-                if (popup.canShow()) {
-                    popup.show();
-                    return true;
-                }
             }
-        }
-        if (host instanceof AccessibilityActionHandler
-                && ((AccessibilityActionHandler) host).performAccessibilityAction(action, item)) {
-            return true;
-        }
-        if (action == MOVE) {
-            beginAccessibleDrag(host, item);
+        } else if (action == MOVE) {
+            return beginAccessibleDrag(host, item, fromKeyboard);
         } else if (action == ADD_TO_WORKSPACE) {
             final int[] coordinates = new int[2];
             final int screenId = findSpaceOnWorkspace(item, coordinates);
@@ -219,9 +229,7 @@
                                 Favorites.CONTAINER_DESKTOP,
                                 screenId, coordinates[0], coordinates[1]);
 
-                        ArrayList<ItemInfo> itemList = new ArrayList<>();
-                        itemList.add(info);
-                        mLauncher.bindItems(itemList, true);
+                        mLauncher.bindItems(Collections.singletonList(info), true);
                         announceConfirmation(R.string.item_added_to_workspace);
                     } else if (item instanceof PendingAddItemInfo) {
                         PendingAddItemInfo info = (PendingAddItemInfo) item;
@@ -242,47 +250,31 @@
             final int[] coordinates = new int[2];
             final int screenId = findSpaceOnWorkspace(item, coordinates);
             mLauncher.getModelWriter().moveItemInDatabase(info,
-                    LauncherSettings.Favorites.CONTAINER_DESKTOP,
+                    Favorites.CONTAINER_DESKTOP,
                     screenId, coordinates[0], coordinates[1]);
 
             // Bind the item in next frame so that if a new workspace page was created,
             // it will get laid out.
-            new Handler().post(new Runnable() {
-
-                @Override
-                public void run() {
-                    ArrayList<ItemInfo> itemList = new ArrayList<>();
-                    itemList.add(item);
-                    mLauncher.bindItems(itemList, true);
-                    announceConfirmation(R.string.item_moved);
-                }
+            new Handler().post(() -> {
+                mLauncher.bindItems(Collections.singletonList(item), true);
+                announceConfirmation(R.string.item_moved);
             });
+            return true;
         } else if (action == RESIZE) {
             final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) item;
-            final IntArray actions = getSupportedResizeActions(host, info);
-            CharSequence[] labels = new CharSequence[actions.size()];
-            for (int i = 0; i < actions.size(); i++) {
-                labels[i] = mLauncher.getText(actions.get(i));
-            }
-
-            new AlertDialog.Builder(mLauncher)
-                .setTitle(R.string.action_resize)
-                .setItems(labels, new DialogInterface.OnClickListener() {
-
-                    @Override
-                    public void onClick(DialogInterface dialog, int which) {
-                        performResizeAction(actions.get(which), host, info);
-                        dialog.dismiss();
-                    }
-                })
-                .show();
+            List<OptionItem> actions = getSupportedResizeActions(host, info);
+            Rect pos = new Rect();
+            mLauncher.getDragLayer().getDescendantRectRelativeToSelf(host, pos);
+            ArrowPopup popup = OptionsPopupView.show(mLauncher, new RectF(pos), actions);
+            popup.requestFocus();
+            popup.setOnCloseCallback(host::requestFocus);
             return true;
-        } else if (action == DEEP_SHORTCUTS) {
+        } else if (action == DEEP_SHORTCUTS || action == SHORTCUTS_AND_NOTIFICATIONS) {
             return PopupContainerWithArrow.showForIcon((BubbleTextView) host) != null;
         } else {
             for (ButtonDropTarget dropTarget : mLauncher.getDropTargetBar().getDropTargets()) {
-                if (dropTarget.supportsAccessibilityDrop(item, host) &&
-                        action == dropTarget.getAccessibilityAction()) {
+                if (dropTarget.supportsAccessibilityDrop(item, host)
+                        && action == dropTarget.getAccessibilityAction()) {
                     dropTarget.onAccessibilityDrop(host, item);
                     return true;
                 }
@@ -291,9 +283,8 @@
         return false;
     }
 
-    private IntArray getSupportedResizeActions(View host, LauncherAppWidgetInfo info) {
-        IntArray actions = new IntArray();
-
+    private List<OptionItem> getSupportedResizeActions(View host, LauncherAppWidgetInfo info) {
+        List<OptionItem> actions = new ArrayList<>();
         AppWidgetProviderInfo providerInfo = ((LauncherAppWidgetHostView) host).getAppWidgetInfo();
         if (providerInfo == null) {
             return actions;
@@ -303,28 +294,40 @@
         if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0) {
             if (layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY) ||
                     layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) {
-                actions.add(R.string.action_increase_width);
+                actions.add(new OptionItem(
+                        R.string.action_increase_width, R.drawable.ic_widget_width_increase,
+                        IGNORE,
+                        v -> performResizeAction(R.string.action_increase_width, host, info)));
             }
 
             if (info.spanX > info.minSpanX && info.spanX > 1) {
-                actions.add(R.string.action_decrease_width);
+                actions.add(new OptionItem(
+                        R.string.action_decrease_width, R.drawable.ic_widget_width_decrease,
+                        IGNORE,
+                        v -> performResizeAction(R.string.action_decrease_width, host, info)));
             }
         }
 
         if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0) {
             if (layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1) ||
                     layout.isRegionVacant(info.cellX, info.cellY - 1, info.spanX, 1)) {
-                actions.add(R.string.action_increase_height);
+                actions.add(new OptionItem(
+                        R.string.action_increase_height, R.drawable.ic_widget_height_increase,
+                        IGNORE,
+                        v -> performResizeAction(R.string.action_increase_height, host, info)));
             }
 
             if (info.spanY > info.minSpanY && info.spanY > 1) {
-                actions.add(R.string.action_decrease_height);
+                actions.add(new OptionItem(
+                        R.string.action_decrease_height, R.drawable.ic_widget_height_decrease,
+                        IGNORE,
+                        v -> performResizeAction(R.string.action_decrease_height, host, info)));
             }
         }
         return actions;
     }
 
-    @Thunk void performResizeAction(int action, View host, LauncherAppWidgetInfo info) {
+    private boolean performResizeAction(int action, View host, LauncherAppWidgetInfo info) {
         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) host.getLayoutParams();
         CellLayout layout = (CellLayout) host.getParent().getParent();
         layout.markCellsAsUnoccupiedForView(host);
@@ -354,13 +357,12 @@
         }
 
         layout.markCellsAsOccupiedForView(host);
-        Rect sizeRange = new Rect();
-        AppWidgetResizeFrame.getWidgetSizeRanges(mLauncher, info.spanX, info.spanY, sizeRange);
-        ((LauncherAppWidgetHostView) host).updateAppWidgetSize(null,
-                sizeRange.left, sizeRange.top, sizeRange.right, sizeRange.bottom);
+        AppWidgetResizeFrame.updateWidgetSizeRanges(((LauncherAppWidgetHostView) host), mLauncher,
+                info.spanX, info.spanY);
         host.requestLayout();
         mLauncher.getModelWriter().updateItemInDatabase(info);
         announceConfirmation(mLauncher.getString(R.string.widget_resized, info.spanX, info.spanY));
+        return true;
     }
 
     @Thunk void announceConfirmation(int resId) {
@@ -406,7 +408,11 @@
         }
     }
 
-    public void beginAccessibleDrag(View item, ItemInfo info) {
+    private boolean beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard) {
+        if (!itemSupportsAccessibleDrag(info)) {
+            return false;
+        }
+
         mDragInfo = new DragInfo();
         mDragInfo.info = info;
         mDragInfo.item = item;
@@ -423,8 +429,17 @@
 
         DragOptions options = new DragOptions();
         options.isAccessibleDrag = true;
+        options.isKeyboardDrag = fromKeyboard;
         options.simulatedDndStartPoint = new Point(pos.centerX(), pos.centerY());
-        ItemLongClickListener.beginDrag(item, mLauncher, info, options);
+
+        if (fromKeyboard) {
+            KeyboardDragAndDropView popup = (KeyboardDragAndDropView) mLauncher.getLayoutInflater()
+                    .inflate(R.layout.keyboard_drag_and_drop, mLauncher.getDragLayer(), false);
+            popup.showForIcon(item, info, options);
+        } else {
+            ItemLongClickListener.beginDrag(item, mLauncher, info, options);
+        }
+        return true;
     }
 
     @Override
@@ -475,19 +490,28 @@
         return screenId;
     }
 
-    /**
-     * An interface allowing views to handle their own action.
-     */
-    public interface AccessibilityActionHandler {
+    public class LauncherAction {
+        public final int keyCode;
+        public final AccessibilityAction accessibilityAction;
+
+        private final LauncherAccessibilityDelegate mDelegate;
+
+        public LauncherAction(int id, int labelRes, int keyCode) {
+            this.keyCode = keyCode;
+            accessibilityAction = new AccessibilityAction(id, mLauncher.getString(labelRes));
+            mDelegate = LauncherAccessibilityDelegate.this;
+        }
 
         /**
-         * performs accessibility action and returns true on success
+         * Invokes the action for the provided host
          */
-        boolean performAccessibilityAction(int action, ItemInfo itemInfo);
-
-        /**
-         * adds all the accessibility actions that can be handled.
-         */
-        void addSupportedAccessibilityActions(AccessibilityNodeInfo accessibilityNodeInfo);
+        public boolean invokeFromKeyboard(View host) {
+            if (host != null && host.getTag() instanceof ItemInfo) {
+                return mDelegate.performAction(
+                        host, (ItemInfo) host.getTag(), accessibilityAction.getId(), true);
+            } else {
+                return false;
+            }
+        }
     }
 }
diff --git a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
index d4ba11e..1733e5d 100644
--- a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java
@@ -18,9 +18,8 @@
 
 import static com.android.launcher3.LauncherState.NORMAL;
 
+import android.view.KeyEvent;
 import android.view.View;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
@@ -31,7 +30,8 @@
 import com.android.launcher3.notification.NotificationMainView;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 
-import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 /**
  * Extension of {@link LauncherAccessibilityDelegate} with actions specific to shortcuts in
@@ -43,23 +43,23 @@
 
     public ShortcutMenuAccessibilityDelegate(Launcher launcher) {
         super(launcher);
-        mActions.put(DISMISS_NOTIFICATION, new AccessibilityAction(DISMISS_NOTIFICATION,
-                launcher.getText(R.string.action_dismiss_notification)));
+        mActions.put(DISMISS_NOTIFICATION, new LauncherAction(DISMISS_NOTIFICATION,
+                R.string.action_dismiss_notification, KeyEvent.KEYCODE_X));
     }
 
     @Override
-    public void addSupportedActions(View host, AccessibilityNodeInfo info, boolean fromKeyboard) {
+    protected void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out) {
         if ((host.getParent() instanceof DeepShortcutView)) {
-            info.addAction(mActions.get(ADD_TO_WORKSPACE));
+            out.add(mActions.get(ADD_TO_WORKSPACE));
         } else if (host instanceof NotificationMainView) {
             if (((NotificationMainView) host).canChildBeDismissed()) {
-                info.addAction(mActions.get(DISMISS_NOTIFICATION));
+                out.add(mActions.get(DISMISS_NOTIFICATION));
             }
         }
     }
 
     @Override
-    public boolean performAction(View host, ItemInfo item, int action) {
+    protected boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) {
         if (action == ADD_TO_WORKSPACE) {
             if (!(host.getParent() instanceof DeepShortcutView)) {
                 return false;
@@ -73,9 +73,7 @@
                     mLauncher.getModelWriter().addItemToDatabase(info,
                             LauncherSettings.Favorites.CONTAINER_DESKTOP,
                             screenId, coordinates[0], coordinates[1]);
-                    ArrayList<ItemInfo> itemList = new ArrayList<>();
-                    itemList.add(info);
-                    mLauncher.bindItems(itemList, true);
+                    mLauncher.bindItems(Collections.singletonList(info), true);
                     AbstractFloatingView.closeAllOpenViews(mLauncher);
                     announceConfirmation(R.string.item_added_to_workspace);
                 }
diff --git a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
index 65a261d..a331924 100644
--- a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
+++ b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
@@ -17,17 +17,12 @@
 package com.android.launcher3.accessibility;
 
 import android.content.Context;
-import android.graphics.Rect;
 import android.text.TextUtils;
 import android.view.View;
 
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
-
 import com.android.launcher3.CellLayout;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.DragType;
-import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
@@ -38,9 +33,6 @@
  */
 public class WorkspaceAccessibilityHelper extends DragAndDropAccessibilityDelegate {
 
-    private final Rect mTempRect = new Rect();
-    private final int[] mTempCords = new int[2];
-
     public WorkspaceAccessibilityHelper(CellLayout layout) {
         super(layout);
     }
@@ -134,26 +126,6 @@
         }
         return "";
     }
-
-    @Override
-    protected void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) {
-        super.onPopulateNodeForVirtualView(id, node);
-
-
-        // ExploreByTouchHelper does not currently handle view scale.
-        // Update BoundsInScreen to appropriate value.
-        DragLayer dragLayer = Launcher.getLauncher(mView.getContext()).getDragLayer();
-        mTempCords[0] = mTempCords[1] = 0;
-        float scale = dragLayer.getDescendantCoordRelativeToSelf(mView, mTempCords);
-
-        node.getBoundsInParent(mTempRect);
-        mTempRect.left = mTempCords[0] + (int) (mTempRect.left * scale);
-        mTempRect.right = mTempCords[0] + (int) (mTempRect.right * scale);
-        mTempRect.top = mTempCords[1] + (int) (mTempRect.top * scale);
-        mTempRect.bottom = mTempCords[1] + (int) (mTempRect.bottom * scale);
-        node.setBoundsInScreen(mTempRect);
-    }
-
     @Override
     protected String getLocationDescriptionForIconDrop(int id) {
         int x = id % mView.getCountX();
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 20f7c23..edd9a9f 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -16,6 +16,8 @@
 package com.android.launcher3.allapps;
 
 import static com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_HAS_SHORTCUT_PERMISSION;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_CHANGE_PERMISSION;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
@@ -32,6 +34,7 @@
 import android.text.Selection;
 import android.text.SpannableStringBuilder;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -61,18 +64,20 @@
 import com.android.launcher3.keyboard.FocusedItemDecorator;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 import com.android.launcher3.views.SpringRelativeLayout;
+import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
 
 /**
  * The all apps view container.
  */
 public class AllAppsContainerView extends SpringRelativeLayout implements DragSource,
-        Insettable, OnDeviceProfileChangeListener {
+        Insettable, OnDeviceProfileChangeListener, OnActivePageChangedListener {
 
     private static final float FLING_VELOCITY_MULTIPLIER = 135f;
     // Starts the springs after at least 55% of the animation has passed.
@@ -194,7 +199,9 @@
                 break;
             }
         }
-        rebindAdapters(hasWorkApps);
+        if (!mAH[AdapterHolder.MAIN].appsList.hasFilter()) {
+            rebindAdapters(hasWorkApps);
+        }
         if (hasWorkApps) {
             resetWorkProfile();
         }
@@ -213,10 +220,14 @@
         if (insets == null) return;
 
         if (insets.isVisible(WindowInsets.Type.ime())) {
-            getWindowInsetsController().hide(WindowInsets.Type.ime());
+            hideIme();
         }
     }
 
+    protected void hideIme() {
+        getWindowInsetsController().hide(WindowInsets.Type.ime());
+    }
+
     /**
      * Returns whether the view itself will handle the touch event or not.
      */
@@ -427,10 +438,20 @@
             mAH[AdapterHolder.WORK].setup(mViewPager.getChildAt(1), mWorkMatcher);
             mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.MAIN);
             findViewById(R.id.tab_personal)
-                    .setOnClickListener((View view) -> mViewPager.snapToPage(AdapterHolder.MAIN));
+                    .setOnClickListener((View view) -> {
+                        if (mViewPager.snapToPage(AdapterHolder.MAIN)) {
+                            mLauncher.getStatsLogManager().logger()
+                                    .log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB);
+                        }
+                    });
             findViewById(R.id.tab_work)
-                    .setOnClickListener((View view) -> mViewPager.snapToPage(AdapterHolder.WORK));
-            onTabChanged(mViewPager.getNextPage());
+                    .setOnClickListener((View view) -> {
+                        if (mViewPager.snapToPage(AdapterHolder.WORK)) {
+                            mLauncher.getStatsLogManager().logger()
+                                    .log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB);
+                        }
+                    });
+            onActivePageChanged(mViewPager.getNextPage());
         } else {
             mAH[AdapterHolder.MAIN].setup(findViewById(R.id.apps_list_view), null);
             mAH[AdapterHolder.WORK].recyclerView = null;
@@ -476,10 +497,14 @@
         int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout;
         View newView = getLayoutInflater().inflate(layout, this, false);
         addView(newView, index);
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.WORK_PROFILE_REMOVED, "should show tabs:" + showTabs,
+                    new Exception());
+        }
         if (showTabs) {
             mViewPager = (AllAppsPagedView) newView;
             mViewPager.initParentViews(this);
-            mViewPager.getPageIndicator().setContainerView(this);
+            mViewPager.getPageIndicator().setOnActivePageChangedListener(this);
         } else {
             mViewPager = null;
         }
@@ -489,14 +514,15 @@
         return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view);
     }
 
-    public void onTabChanged(int pos) {
-        mHeader.setMainActive(pos == 0);
-        if (mAH[pos].recyclerView != null) {
-            mAH[pos].recyclerView.bindFastScrollbar();
+    @Override
+    public void onActivePageChanged(int currentActivePage) {
+        mHeader.setMainActive(currentActivePage == 0);
+        if (mAH[currentActivePage].recyclerView != null) {
+            mAH[currentActivePage].recyclerView.bindFastScrollbar();
         }
         reset(true /* animate */);
         if (mWorkModeSwitch != null) {
-            mWorkModeSwitch.setWorkTabVisible(pos == AdapterHolder.WORK
+            mWorkModeSwitch.setWorkTabVisible(currentActivePage == AdapterHolder.WORK
                     && mAllAppsStore.hasModelFlag(
                     FLAG_HAS_SHORTCUT_PERMISSION | FLAG_QUIET_MODE_CHANGE_PERMISSION));
         }
@@ -751,6 +777,7 @@
                 int bottomOffset = mWorkModeSwitch != null && mIsWork ? switchH : 0;
                 recyclerView.setPadding(padding.left, padding.top, padding.right,
                         padding.bottom + bottomOffset);
+                recyclerView.scrollToTop();
             }
         }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index 715c142..bb175ea 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -42,8 +42,7 @@
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.search.SearchAdapterProvider;
-import com.android.launcher3.allapps.search.SearchSectionInfo;
-import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.allapps.search.SectionDecorationInfo;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.util.PackageManagerHelper;
 
@@ -109,7 +108,7 @@
         // The index of this app not including sections
         public int appIndex = -1;
         // Search section associated to result
-        public SearchSectionInfo searchSectionInfo = null;
+        public SectionDecorationInfo sectionDecorationInfo = null;
 
         /**
          * Factory method for AppIcon AdapterItem
@@ -241,20 +240,18 @@
         @Override
         public int getSpanSize(int position) {
             int viewType = mApps.getAdapterItems().get(position).viewType;
+            int totalSpans = mGridLayoutMgr.getSpanCount();
             if (isIconViewType(viewType)) {
-                return 1 * SPAN_MULTIPLIER;
+                return totalSpans / mAppsPerRow;
             } else if (mSearchAdapterProvider.isSearchView(viewType)) {
-                return mSearchAdapterProvider.getGridSpanSize(viewType, mAppsPerRow);
+                return totalSpans / mSearchAdapterProvider.getItemsPerRow(viewType, mAppsPerRow);
             } else {
                 // Section breaks span the full width
-                return mAppsPerRow * SPAN_MULTIPLIER;
+                return totalSpans;
             }
         }
     }
 
-    // multiplier to support adapter item column count that is not mAppsPerRow.
-    public static final int SPAN_MULTIPLIER = 3;
-
     private final BaseDraggingActivity mLauncher;
     private final LayoutInflater mLayoutInflater;
     private final AlphabeticalAppsList mApps;
@@ -286,14 +283,19 @@
 
         mOnIconClickListener = launcher.getItemOnClickListener();
 
-        setAppsPerRow(mLauncher.getDeviceProfile().inv.numAllAppsColumns);
-
         mSearchAdapterProvider = searchAdapterProvider;
+        setAppsPerRow(mLauncher.getDeviceProfile().inv.numAllAppsColumns);
     }
 
     public void setAppsPerRow(int appsPerRow) {
         mAppsPerRow = appsPerRow;
-        mGridLayoutMgr.setSpanCount(mAppsPerRow * SPAN_MULTIPLIER);
+        int totalSpans = mAppsPerRow;
+        for (int itemPerRow : mSearchAdapterProvider.getSupportedItemsPerRowArray()) {
+            if (totalSpans % itemPerRow != 0) {
+                totalSpans *= itemPerRow;
+            }
+        }
+        mGridLayoutMgr.setSpanCount(totalSpans);
     }
 
     /**
@@ -372,10 +374,6 @@
 
     @Override
     public void onBindViewHolder(ViewHolder holder, int position) {
-        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()
-                && holder.itemView instanceof AllAppsSectionDecorator.SelfDecoratingView) {
-            ((AllAppsSectionDecorator.SelfDecoratingView) holder.itemView).removeDecoration();
-        }
         switch (holder.getItemViewType()) {
             case VIEW_TYPE_ICON:
                 AdapterItem adapterItem = mApps.getAdapterItems().get(position);
@@ -409,10 +407,6 @@
     @Override
     public void onViewRecycled(@NonNull ViewHolder holder) {
         super.onViewRecycled(holder);
-        if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get()) return;
-        if (holder.itemView instanceof AllAppsSectionDecorator.SelfDecoratingView) {
-            ((AllAppsSectionDecorator.SelfDecoratingView) holder.itemView).removeDecoration();
-        }
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/AllAppsInsetTransitionController.java b/src/com/android/launcher3/allapps/AllAppsInsetTransitionController.java
index 1cf98e1..b34c8b8 100644
--- a/src/com/android/launcher3/allapps/AllAppsInsetTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsInsetTransitionController.java
@@ -86,6 +86,10 @@
         mApps = appsView;
     }
 
+    public void show() {
+        mApps.getWindowInsetsController().show(WindowInsets.Type.ime());
+    }
+
     public void hide() {
         if (!Utilities.ATLEAST_R) return;
 
@@ -164,8 +168,11 @@
                     @Override
                     public void onCancelled(@Nullable WindowInsetsAnimationController controller) {
                         if (DEBUG) {
-                            Log.d(TAG, "Listener.onCancelled ctrl=" + controller
-                                    + " mAnimationController=" + mAnimationController);
+                            // Keep the verbose logging to chase down IME not showing up issue.
+                            // b/178904132
+                            Log.e(TAG, "Listener.onCancelled ctrl=" + controller
+                                    + " mAnimationController=" + mAnimationController,
+                                    new Exception());
                         }
                         if (mState == State.DRAG_START_BOTTOM) {
                             mState = State.DRAG_START_BOTTOM_IME_CANCELLED;
diff --git a/src/com/android/launcher3/allapps/AllAppsPagedView.java b/src/com/android/launcher3/allapps/AllAppsPagedView.java
index e2550f5..14e3b51 100644
--- a/src/com/android/launcher3/allapps/AllAppsPagedView.java
+++ b/src/com/android/launcher3/allapps/AllAppsPagedView.java
@@ -15,19 +15,23 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SWIPE_TO_PERSONAL_TAB;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SWIPE_TO_WORK_TAB;
+
 import android.content.Context;
 import android.util.AttributeSet;
-import android.view.MotionEvent;
 
+import com.android.launcher3.Launcher;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.workprofile.PersonalWorkPagedView;
 
-public class AllAppsPagedView extends PagedView<PersonalWorkSlidingTabStrip> {
-
-    static final float START_DAMPING_TOUCH_SLOP_ANGLE = (float) Math.PI / 6;
-    static final float MAX_SWIPE_ANGLE = (float) Math.PI / 3;
-    static final float TOUCH_SLOP_DAMPING_FACTOR = 4;
+/**
+ *  A {@link PagedView} for showing different views for the personal and work profile respectively
+ *  in the {@link AllAppsContainerView}.
+ */
+public class AllAppsPagedView extends PersonalWorkPagedView {
 
     public AllAppsPagedView(Context context) {
         this(context, null);
@@ -46,50 +50,14 @@
     }
 
     @Override
-    protected String getCurrentPageDescription() {
-        // Not necessary, tab-bar already has two tabs with their own descriptions.
-        return "";
-    }
-
-    @Override
-    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
-        super.onScrollChanged(l, t, oldl, oldt);
-        mPageIndicator.setScroll(l, mMaxScroll);
-    }
-
-    @Override
-    protected void determineScrollingStart(MotionEvent ev) {
-        float absDeltaX = Math.abs(ev.getX() - getDownMotionX());
-        float absDeltaY = Math.abs(ev.getY() - getDownMotionY());
-
-        if (Float.compare(absDeltaX, 0f) == 0) return;
-
-        float slope = absDeltaY / absDeltaX;
-        float theta = (float) Math.atan(slope);
-
-        if (absDeltaX > mTouchSlop || absDeltaY > mTouchSlop) {
-            cancelCurrentPageLongPress();
+    protected boolean snapToPageWithVelocity(int whichPage, int velocity) {
+        boolean resp = super.snapToPageWithVelocity(whichPage, velocity);
+        if (resp && whichPage != mCurrentPage) {
+            Launcher.getLauncher(getContext()).getStatsLogManager().logger()
+                    .log(mCurrentPage < whichPage
+                            ? LAUNCHER_ALLAPPS_SWIPE_TO_WORK_TAB
+                            : LAUNCHER_ALLAPPS_SWIPE_TO_PERSONAL_TAB);
         }
-
-        if (theta > MAX_SWIPE_ANGLE) {
-            return;
-        } else if (theta > START_DAMPING_TOUCH_SLOP_ANGLE) {
-            theta -= START_DAMPING_TOUCH_SLOP_ANGLE;
-            float extraRatio = (float)
-                    Math.sqrt((theta / (MAX_SWIPE_ANGLE - START_DAMPING_TOUCH_SLOP_ANGLE)));
-            super.determineScrollingStart(ev, 1 + TOUCH_SLOP_DAMPING_FACTOR * extraRatio);
-        } else {
-            super.determineScrollingStart(ev);
-        }
-    }
-
-    @Override
-    public boolean hasOverlappingRendering() {
-        return false;
-    }
-
-    @Override
-    protected boolean canScroll(float absVScroll, float absHScroll) {
-        return (absHScroll > absVScroll) && super.canScroll(absVScroll, absHScroll);
+        return resp;
     }
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index e61b95d..179cb77 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -19,6 +19,9 @@
 import static android.view.View.MeasureSpec.UNSPECIFIED;
 import static android.view.View.MeasureSpec.makeMeasureSpec;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END;
+
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
@@ -36,7 +39,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
-import com.android.launcher3.allapps.AllAppsGridAdapter.AppsGridLayoutManager;
+import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 
 import java.util.ArrayList;
@@ -109,23 +112,6 @@
         mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, grid.allAppsCellHeightPx);
     }
 
-    /**
-     * Scrolls this recycler view to the top.
-     */
-    public void scrollToTop() {
-        // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling
-        if (mScrollbar != null) {
-            mScrollbar.reattachThumbToScroll();
-        }
-        if (getLayoutManager() instanceof AppsGridLayoutManager) {
-            AppsGridLayoutManager layoutManager = (AppsGridLayoutManager) getLayoutManager();
-            if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
-                // We are at the top, so don't scrollToPosition (would cause unnecessary relayout).
-                return;
-            }
-        }
-        scrollToPosition(0);
-    }
 
     @Override
     public void onDraw(Canvas c) {
@@ -194,6 +180,23 @@
     }
 
     @Override
+    public void onScrollStateChanged(int state) {
+        super.onScrollStateChanged(state);
+
+        StatsLogManager mgr = BaseDraggingActivity.fromContext(getContext()).getStatsLogManager();
+        switch (state) {
+            case SCROLL_STATE_DRAGGING:
+                mgr.logger().sendToInteractionJankMonitor(
+                        LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN, this);
+                break;
+            case SCROLL_STATE_IDLE:
+                mgr.logger().sendToInteractionJankMonitor(
+                        LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END, this);
+                break;
+        }
+    }
+
+    @Override
     public boolean onInterceptTouchEvent(MotionEvent e) {
         boolean result = super.onInterceptTouchEvent(e);
         if (!result && e.getAction() == MotionEvent.ACTION_DOWN
diff --git a/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java b/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java
index 1d31975..f4d735e 100644
--- a/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java
+++ b/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java
@@ -18,16 +18,18 @@
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Paint;
+import android.graphics.Path;
 import android.graphics.RectF;
 import android.view.View;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.core.graphics.ColorUtils;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.AllAppsGridAdapter.AppsGridLayoutManager;
-import com.android.launcher3.allapps.search.SearchSectionInfo;
+import com.android.launcher3.allapps.search.SectionDecorationInfo;
 import com.android.launcher3.util.Themes;
 
 import java.util.List;
@@ -45,52 +47,34 @@
 
     @Override
     public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
-        // Iterate through views in recylerview and draw bounds around views in the same section.
-        // Since views in the same section will follow each other, we can skip to a last view in
-        // a section to get the bounds of the section without having to iterate on every item.
-        int itemCount = parent.getChildCount();
         List<AllAppsGridAdapter.AdapterItem> adapterItems = mAppsView.getApps().getAdapterItems();
-        SectionDecorationHandler lastDecorationHandler = null;
-        int i = 0;
-        while (i < itemCount) {
+        for (int i = 0; i < parent.getChildCount(); i++) {
             View view = parent.getChildAt(i);
-            if (view instanceof SelfDecoratingView) {
-                ((SelfDecoratingView) view).removeDecoration();
-            }
             int position = parent.getChildAdapterPosition(view);
             AllAppsGridAdapter.AdapterItem adapterItem = adapterItems.get(position);
-            if (adapterItem.searchSectionInfo != null) {
-                SearchSectionInfo sectionInfo = adapterItem.searchSectionInfo;
-                int endIndex = Math.min(i + sectionInfo.getPosEnd() - position, itemCount - 1);
+            if (adapterItem.sectionDecorationInfo != null) {
+                SectionDecorationInfo sectionInfo = adapterItem.sectionDecorationInfo;
                 SectionDecorationHandler decorationHandler = sectionInfo.getDecorationHandler();
-                if (decorationHandler != lastDecorationHandler && lastDecorationHandler != null) {
-                    drawDecoration(c, lastDecorationHandler, parent);
-                }
-                lastDecorationHandler = decorationHandler;
                 if (decorationHandler != null) {
                     decorationHandler.extendBounds(view);
-                }
-
-                if (endIndex > i) {
-                    i = endIndex;
-                    continue;
+                    if (sectionInfo.isFocusedView()) {
+                        decorationHandler.onFocusDraw(c, view);
+                    } else {
+                        decorationHandler.onGroupDraw(c);
+                    }
                 }
             }
-            i++;
-        }
-        if (lastDecorationHandler != null) {
-            drawDecoration(c, lastDecorationHandler, parent);
         }
     }
 
-    private void drawDecoration(Canvas c, SectionDecorationHandler decorationHandler,
-            RecyclerView parent) {
-        if (decorationHandler == null) return;
+    // Fallback logic in case non of the SearchTarget is labeled as focused item.
+    private void drawDecoration(@NonNull Canvas c,
+            @NonNull SectionDecorationHandler decorationHandler,
+            @NonNull RecyclerView parent) {
         if (decorationHandler.mIsFullWidth) {
             decorationHandler.mBounds.left = parent.getPaddingLeft();
             decorationHandler.mBounds.right = parent.getWidth() - parent.getPaddingRight();
         }
-        decorationHandler.onDraw(c);
         if (mAppsView.getFloatingHeaderView().getFocusedChild() == null
                 && mAppsView.getApps().getFocusedChild() != null) {
             int index = mAppsView.getApps().getFocusedChildIndex();
@@ -109,24 +93,41 @@
      * Handles grouping and drawing of items in the same all apps sections.
      */
     public static class SectionDecorationHandler {
-        private static final int FILL_ALPHA = 0;
-        private static final int FOCUS_ALPHA = (int) (.9f * 255);
-
         protected RectF mBounds = new RectF();
         private final boolean mIsFullWidth;
         private final float mRadius;
 
-        protected int mFocusColor;
-        protected int mFillcolor;
+        protected final int mFocusColor; // main focused item color
+        protected final int mFillcolor; // grouping color
+
         private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        private final boolean mIsTopRound;
+        private final boolean mIsBottomRound;
+        private float [] mCorners;
+        private float mFillSpacing;
 
+        public SectionDecorationHandler(Context context, boolean isFullWidth, int fillAlpha,
+                boolean isTopRound, boolean isBottomRound) {
 
-        public SectionDecorationHandler(Context context, boolean isFullWidth) {
             mIsFullWidth = isFullWidth;
-            int endScrim = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
-            mFillcolor = ColorUtils.setAlphaComponent(endScrim, FILL_ALPHA);
-            mFocusColor = ColorUtils.setAlphaComponent(endScrim, FOCUS_ALPHA);
-            mRadius = Themes.getDialogCornerRadius(context);
+            int endScrim = Themes.getColorBackground(context);
+            mFillcolor = ColorUtils.setAlphaComponent(endScrim, fillAlpha);
+            mFocusColor = endScrim;
+
+            mIsTopRound = isTopRound;
+            mIsBottomRound = isBottomRound;
+
+            mRadius = context.getResources().getDimensionPixelSize(
+                    R.dimen.search_decoration_corner_radius);
+            mFillSpacing = context.getResources().getDimensionPixelSize(
+                    R.dimen.search_decoration_padding);
+            mCorners = new float[]{
+                    mIsTopRound ? mRadius : 0, mIsTopRound ? mRadius : 0, // Top left radius in px
+                    mIsTopRound ? mRadius : 0, mIsTopRound ? mRadius : 0, // Top right radius in px
+                    mIsBottomRound ? mRadius : 0, mIsBottomRound ? mRadius : 0, // Bottom right
+                    mIsBottomRound ? mRadius : 0, mIsBottomRound ? mRadius : 0  // Bottom left
+            };
+
         }
 
         /**
@@ -148,9 +149,9 @@
         /**
          * Draw bounds onto canvas.
          */
-        public void onDraw(Canvas canvas) {
+        public void onGroupDraw(Canvas canvas) {
             mPaint.setColor(mFillcolor);
-            canvas.drawRoundRect(mBounds, mRadius, mRadius, mPaint);
+            onDraw(canvas);
         }
 
         /**
@@ -160,13 +161,20 @@
             if (view == null) {
                 return;
             }
-            if (view instanceof SelfDecoratingView) {
-                ((SelfDecoratingView) view).decorate(mFocusColor);
-                return;
-            }
             mPaint.setColor(mFocusColor);
-            canvas.drawRoundRect(view.getLeft(), view.getTop(),
-                    view.getRight(), view.getBottom(), mRadius, mRadius, mPaint);
+            mBounds.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
+            onDraw(canvas);
+        }
+
+
+        private void onDraw(Canvas canvas) {
+            final Path path = new Path();
+            RectF finalBounds = new RectF(mBounds.left + mFillSpacing,
+                    mBounds.top + mFillSpacing,
+                    mBounds.right - mFillSpacing,
+                    mBounds.bottom - mFillSpacing);
+            path.addRoundRect(finalBounds, mCorners, Path.Direction.CW);
+            canvas.drawPath(path, mPaint);
         }
 
         /**
@@ -176,19 +184,4 @@
             mBounds.setEmpty();
         }
     }
-
-    /**
-     * An interface for a view to draw highlight indicator
-     */
-    public interface SelfDecoratingView {
-        /**
-         * Removes decorations drawing if focus is acquired by another view
-         */
-        void removeDecoration();
-
-        /**
-         * Draws highlight indicator on view.
-         */
-        void decorate(int focusColor);
-    }
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index 769cb5e..355ccad 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -21,6 +21,8 @@
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
@@ -78,6 +80,11 @@
         return (mModelFlags & mask) != 0;
     }
 
+    /**
+     * Returns {@link AppInfo} if any apps matches with provided {@link ComponentKey}, otherwise
+     * null.
+     */
+    @Nullable
     public AppInfo getApp(ComponentKey key) {
         mTempInfo.componentName = key.componentName;
         mTempInfo.user = key.user;
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 2f805fd..a48e423 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -33,6 +33,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
+import android.content.SharedPreferences;
 import android.util.FloatProperty;
 import android.view.View;
 import android.view.animation.Interpolator;
@@ -63,7 +64,7 @@
  * closer to top or closer to the page indicator.
  */
 public class AllAppsTransitionController implements StateHandler<LauncherState>,
-        OnDeviceProfileChangeListener {
+        OnDeviceProfileChangeListener, SharedPreferences.OnSharedPreferenceChangeListener {
 
     public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
             new FloatProperty<AllAppsTransitionController>("allAppsProgress") {
@@ -80,6 +81,7 @@
             };
 
     private static final int APPS_VIEW_ALPHA_CHANNEL_INDEX = 0;
+    private static final String PREF_KEY_SHOW_SEARCH_IME = "pref_search_show_ime";
 
     private AllAppsContainerView mAppsView;
     private ScrimView mScrimView;
@@ -98,6 +100,7 @@
 
     private float mScrollRangeDelta = 0;
     private AllAppsInsetTransitionController mInsetController;
+    private boolean mSearchImeEnabled;
 
     public AllAppsTransitionController(Launcher l) {
         mLauncher = l;
@@ -106,6 +109,9 @@
 
         mIsVerticalLayout = mLauncher.getDeviceProfile().isVerticalBarLayout();
         mLauncher.addOnDeviceProfileChangeListener(this);
+
+        onSharedPreferenceChanged(mLauncher.getSharedPrefs(), PREF_KEY_SHOW_SEARCH_IME);
+        mLauncher.getSharedPrefs().registerOnSharedPreferenceChangeListener(this);
     }
 
     public float getShiftRange() {
@@ -138,12 +144,14 @@
      */
     public void setProgress(float progress) {
         mProgress = progress;
+
         mScrimView.setProgress(progress);
         float shiftCurrent = progress * mShiftRange;
-
         mAppsView.setTranslationY(shiftCurrent);
-        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()
-                && !FeatureFlags.DISABLE_INITIAL_IME_IN_ALLAPPS.get()) {
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && mSearchImeEnabled) {
+            if (mInsetController == null) {
+                setupInsetTransitionController();
+            }
             mInsetController.setProgress(progress);
         }
     }
@@ -234,16 +242,18 @@
     public void setupViews(AllAppsContainerView appsView, ScrimView scrimView) {
         mAppsView = appsView;
         mScrimView = scrimView;
-        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()
-                && !FeatureFlags.DISABLE_INITIAL_IME_IN_ALLAPPS.get()
-                && BuildCompat.isAtLeastR()) {
-            mInsetController = new AllAppsInsetTransitionController(mShiftRange, mAppsView);
-            mLauncher.getSystemUiController().updateUiState(UI_STATE_ALLAPPS,
-                    View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
-                            | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && BuildCompat.isAtLeastR()) {
+            setupInsetTransitionController();
         }
     }
 
+    private void setupInsetTransitionController() {
+        mInsetController = new AllAppsInsetTransitionController(mShiftRange, mAppsView);
+        mLauncher.getSystemUiController().updateUiState(UI_STATE_ALLAPPS,
+                View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+    }
+
     /**
      * Updates the total scroll range but does not update the UI.
      */
@@ -264,19 +274,23 @@
         if (Float.compare(mProgress, 1f) == 0) {
             mAppsView.reset(false /* animate */);
         }
-        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()
-                && !FeatureFlags.DISABLE_INITIAL_IME_IN_ALLAPPS.get() && BuildCompat.isAtLeastR()) {
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && mSearchImeEnabled
+                && BuildCompat.isAtLeastR()) {
             mInsetController.onAnimationEnd(mProgress);
             if (Float.compare(mProgress, 0f) == 0) {
-                mLauncher.getLiveSearchManager().start();
                 EditText editText = mAppsView.getSearchUiManager().getEditText();
                 if (editText != null && !mInsetController.showSearchEduIfNecessary()) {
                     editText.requestFocus();
                 }
-            } else {
-                mLauncher.getLiveSearchManager().stop();
             }
             // TODO: should make the controller hide synchronously
         }
     }
+
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
+        if (s.equals(PREF_KEY_SHOW_SEARCH_IME)) {
+            mSearchImeEnabled = sharedPreferences.getBoolean(s, true);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 1dc10fe..fefd97a 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -20,7 +20,7 @@
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
-import com.android.launcher3.allapps.search.SearchSectionInfo;
+import com.android.launcher3.allapps.search.SectionDecorationInfo;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.util.ItemInfoMatcher;
@@ -185,7 +185,7 @@
         if (results == null || mSearchResults != results) {
             boolean same = mSearchResults != null && mSearchResults.equals(results);
             mSearchResults = results;
-            onAppsUpdated();
+            updateAdapterItems();
             return !same;
         }
         return false;
@@ -201,20 +201,11 @@
     }
 
     void updateSearchAdapterItems(ArrayList<AdapterItem> list, int offset) {
-        SearchSectionInfo lastSection = null;
         for (int i = 0; i < list.size(); i++) {
             AdapterItem adapterItem = list.get(i);
             adapterItem.position = offset + i;
             mAdapterItems.add(adapterItem);
-            if (adapterItem.searchSectionInfo != lastSection) {
-                if (adapterItem.searchSectionInfo != null) {
-                    adapterItem.searchSectionInfo.setPosStart(adapterItem.position);
-                }
-                if (lastSection != null) {
-                    lastSection.setPosEnd(adapterItem.position - 1);
-                }
-                lastSection = adapterItem.searchSectionInfo;
-            }
+
             if (adapterItem.isCountedForAccessibility()) {
                 mAccessibilityResultsCount++;
             }
@@ -266,11 +257,13 @@
         }
 
         // Recompose the set of adapter items from the current set of apps
-        updateAdapterItems();
+        if (mSearchResults == null) {
+            updateAdapterItems();
+        }
     }
 
     /**
-     * Updates the set of filtered apps with the current filter.  At this point, we expect
+     * Updates the set of filtered apps with the current filter. At this point, we expect
      * mCachedSectionNames to have been calculated for the set of all apps in mApps.
      */
     private void updateAdapterItems() {
@@ -295,16 +288,16 @@
         mFastScrollerSections.clear();
         mAdapterItems.clear();
 
-        SearchSectionInfo appSection = new SearchSectionInfo();
+        SectionDecorationInfo appSection = new SectionDecorationInfo();
         appSection.setDecorationHandler(
-                new AllAppsSectionDecorator.SectionDecorationHandler(mLauncher, true));
+                new AllAppsSectionDecorator.SectionDecorationHandler(mLauncher, true,
+                        0, false, false));
 
         // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
         // ordered set of sections
 
         if (!hasFilter()) {
             mAccessibilityResultsCount = mApps.size();
-            appSection.setPosStart(position);
             for (AppInfo info : mApps) {
                 String sectionName = info.sectionName;
 
@@ -321,11 +314,10 @@
                     lastFastScrollerSectionInfo.fastScrollToItem = appItem;
                 }
                 if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
-                    appItem.searchSectionInfo = appSection;
+                    appItem.sectionDecorationInfo = appSection;
                 }
                 mAdapterItems.add(appItem);
             }
-            appSection.setPosEnd(mApps.isEmpty() ? appSection.getPosStart() : position - 1);
         } else {
             updateSearchAdapterItems(mSearchResults, 0);
             if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderRow.java b/src/com/android/launcher3/allapps/FloatingHeaderRow.java
index e357f61..31c6cc7 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderRow.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderRow.java
@@ -61,4 +61,11 @@
      * Returns a child that has focus to be launched by the IME.
      */
     View getFocusedChild();
+
+    /**
+     * Returns true if view is currently visible
+     */
+    default boolean isVisible() {
+        return shouldDraw();
+    }
 }
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java
index 813db7d..9056e8a 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderView.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java
@@ -200,7 +200,7 @@
     public View getFocusedChild() {
         if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
             for (FloatingHeaderRow row : mAllRows) {
-                if (row.hasVisibleContent() && row.shouldDraw()) {
+                if (row.hasVisibleContent() && row.isVisible()) {
                     return row.getFocusedChild();
                 }
             }
diff --git a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
index a6bc6cf..13ddc12 100644
--- a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_KEYBOARD_CLOSED;
+
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
@@ -83,14 +85,20 @@
     }
 
     @Override
-    public void onTabChanged(int pos) {
-        super.onTabChanged(pos);
+    public void onActivePageChanged(int currentActivePage) {
+        super.onActivePageChanged(currentActivePage);
         if (mUsingTabs) {
-            if (pos == AdapterHolder.WORK) {
+            if (currentActivePage == AdapterHolder.WORK) {
                 WorkEduView.showWorkEduIfNeeded(mLauncher);
             } else {
                 mWorkTabListener = WorkEduView.showEduFlowIfNeeded(mLauncher, mWorkTabListener);
             }
         }
     }
+
+    @Override
+    protected void hideIme() {
+        super.hideIme();
+        mLauncher.getStatsLogManager().logger().log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED);
+    }
 }
diff --git a/src/com/android/launcher3/allapps/SearchUiManager.java b/src/com/android/launcher3/allapps/SearchUiManager.java
index f926086..39410a7 100644
--- a/src/com/android/launcher3/allapps/SearchUiManager.java
+++ b/src/com/android/launcher3/allapps/SearchUiManager.java
@@ -61,7 +61,8 @@
     /**
      * Called when activity is destroyed. Used to close search system services
      */
-    default void destroy(){}
+    default void destroy() {
+    }
 
     /**
      * Returns true if the QSB should be visible for the given set of visible elements
@@ -75,4 +76,9 @@
      */
     @Nullable
     EditText getEditText();
+
+    /**
+     * sets highlight result's title
+     */
+    default void setFocusedResultTitle(@Nullable  CharSequence title) { }
 }
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index 198c4b2..3319018 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.allapps.search;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_FOCUSED_ITEM_SELECTED_WITH_IME;
+
 import android.text.Editable;
 import android.text.TextUtils;
 import android.text.TextWatcher;
@@ -31,10 +33,10 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.search.SearchCallback;
 import com.android.launcher3.util.PackageManagerHelper;
 
-import java.util.ArrayList;
-
 /**
  * An interface to a search box that AllApps can command.
  */
@@ -43,11 +45,11 @@
         OnFocusChangeListener {
 
     protected BaseDraggingActivity mLauncher;
-    protected Callbacks mCb;
+    protected SearchCallback<AdapterItem> mCallback;
     protected ExtendedEditText mInput;
     protected String mQuery;
 
-    protected SearchAlgorithm mSearchAlgorithm;
+    protected SearchAlgorithm<AdapterItem> mSearchAlgorithm;
 
     public void setVisibility(int visibility) {
         mInput.setVisibility(visibility);
@@ -57,9 +59,9 @@
      * Sets the references to the apps model and the search result callback.
      */
     public final void initialize(
-            SearchAlgorithm searchAlgorithm, ExtendedEditText input,
-            BaseDraggingActivity launcher, Callbacks cb) {
-        mCb = cb;
+            SearchAlgorithm<AdapterItem> searchAlgorithm, ExtendedEditText input,
+            BaseDraggingActivity launcher, SearchCallback<AdapterItem> callback) {
+        mCallback = callback;
         mLauncher = launcher;
 
         mInput = input;
@@ -85,10 +87,10 @@
         mQuery = s.toString();
         if (mQuery.isEmpty()) {
             mSearchAlgorithm.cancel(true);
-            mCb.clearSearchResult();
+            mCallback.clearSearchResult();
         } else {
             mSearchAlgorithm.cancel(false);
-            mSearchAlgorithm.doSearch(mQuery, mCb);
+            mSearchAlgorithm.doSearch(mQuery, mCallback);
         }
     }
 
@@ -98,13 +100,15 @@
         }
         // If play store continues auto updating an app, we want to show partial result.
         mSearchAlgorithm.cancel(false);
-        mSearchAlgorithm.doSearch(mQuery, mCb);
+        mSearchAlgorithm.doSearch(mQuery, mCallback);
     }
 
     @Override
     public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
         if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
             if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_GO) {
+                mLauncher.getStatsLogManager().logger()
+                        .log(LAUNCHER_ALLAPPS_FOCUSED_ITEM_SELECTED_WITH_IME);
                 // selectFocusedView should return SearchTargetEvent that is passed onto onClick
                 if (Launcher.getLauncher(mLauncher).getAppsView().selectFocusedView(v)) {
                     return true;
@@ -149,7 +153,7 @@
      * Resets the search bar state.
      */
     public void reset() {
-        mCb.clearSearchResult();
+        mCallback.clearSearchResult();
         mInput.reset();
         mQuery = null;
     }
@@ -167,31 +171,4 @@
     public boolean isSearchFieldFocused() {
         return mInput.isFocused();
     }
-
-    /**
-     * Callback for getting search results.
-     */
-    public interface Callbacks {
-
-        /**
-         * Called when the search from primary source is complete.
-         *
-         * @param items sorted list of search result adapter items
-         */
-        void onSearchResult(String query, ArrayList<AdapterItem> items);
-
-        /**
-         * Called when the search from secondary source is complete.
-         *
-         * @param items sorted list of search result adapter items
-         */
-        void onAppendSearchResult(String query, ArrayList<AdapterItem> items);
-
-        /**
-         * Called when the search results should be cleared.
-         */
-        void clearSearchResult();
-    }
-
-
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
index 4f79fb8..426fd0c 100644
--- a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
+++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
@@ -47,6 +47,7 @@
 import com.android.launcher3.allapps.SearchUiManager;
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.search.SearchCallback;
 
 import java.util.ArrayList;
 
@@ -54,7 +55,7 @@
  * Layout to contain the All-apps search UI.
  */
 public class AppsSearchContainerLayout extends ExtendedEditText
-        implements SearchUiManager, AllAppsSearchBarController.Callbacks,
+        implements SearchUiManager, SearchCallback<AdapterItem>,
         AllAppsStore.OnUpdateListener, Insettable {
 
     private final BaseDraggingActivity mLauncher;
@@ -109,7 +110,8 @@
         int rowWidth = myRequestedWidth - mAppsView.getActiveRecyclerView().getPaddingLeft()
                 - mAppsView.getActiveRecyclerView().getPaddingRight();
 
-        int cellWidth = DeviceProfile.calculateCellWidth(rowWidth, dp.inv.numHotseatIcons);
+        int cellWidth = DeviceProfile.calculateCellWidth(rowWidth, dp.cellLayoutBorderSpacingPx,
+                dp.inv.numHotseatIcons);
         int iconVisibleSize = Math.round(ICON_VISIBLE_AREA_FACTOR * dp.iconSizePx);
         int iconPadding = cellWidth - iconVisibleSize;
 
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java b/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java
index 84688e1..f9fb22e 100644
--- a/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java
+++ b/src/com/android/launcher3/allapps/search/AppsSearchPipeline.java
@@ -37,14 +37,14 @@
 
     private static final int MAX_RESULTS_COUNT = 5;
 
-    private final SearchSectionInfo mSearchSectionInfo;
+    private final SectionDecorationInfo mSearchSectionInfo;
     private final LauncherAppState mLauncherAppState;
 
     public AppsSearchPipeline(Context context, LauncherAppState launcherAppState) {
         mLauncherAppState = launcherAppState;
-        mSearchSectionInfo = new SearchSectionInfo();
+        mSearchSectionInfo = new SectionDecorationInfo();
         mSearchSectionInfo.setDecorationHandler(
-                new SectionDecorationHandler(context, true));
+                new SectionDecorationHandler(context, true, 0, true, true));
     }
 
     @Override
@@ -81,7 +81,7 @@
         ArrayList<AdapterItem> items = new ArrayList<>();
         for (int i = 0; i < matchingApps.size() && i < MAX_RESULTS_COUNT; i++) {
             AdapterItem appItem = AdapterItem.asApp(i, "", matchingApps.get(i), i);
-            appItem.searchSectionInfo = mSearchSectionInfo;
+            appItem.sectionDecorationInfo = mSearchSectionInfo;
             items.add(appItem);
         }
 
diff --git a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
index 66bbd2e..4e213b0 100644
--- a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
+++ b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
@@ -19,14 +19,17 @@
 import android.os.Handler;
 
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
 import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.search.SearchCallback;
 
 import java.text.Collator;
 
 /**
  * The default search implementation.
  */
-public class DefaultAppSearchAlgorithm implements SearchAlgorithm {
+public class DefaultAppSearchAlgorithm implements SearchAlgorithm<AdapterItem> {
 
     protected final Handler mResultHandler;
     private final AppsSearchPipeline mAppsSearchPipeline;
@@ -45,7 +48,7 @@
 
     @Override
     public void doSearch(final String query,
-            final AllAppsSearchBarController.Callbacks callback) {
+            final SearchCallback<AdapterItem> callback) {
         mAppsSearchPipeline.query(query,
                 results -> mResultHandler.post(
                         () -> callback.onSearchResult(query, results)),
diff --git a/src/com/android/launcher3/allapps/search/LiveSearchManager.java b/src/com/android/launcher3/allapps/search/LiveSearchManager.java
index ec33908..76099a6 100644
--- a/src/com/android/launcher3/allapps/search/LiveSearchManager.java
+++ b/src/com/android/launcher3/allapps/search/LiveSearchManager.java
@@ -15,8 +15,14 @@
  */
 package com.android.launcher3.allapps.search;
 
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.widget.WidgetHostViewLoader.getDefaultOptionsForWidget;
 
+import android.app.Activity;
+import android.app.Application.ActivityLifecycleCallbacks;
 import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetManager;
@@ -26,35 +32,46 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.UserHandle;
+import android.util.Log;
 
-import androidx.lifecycle.LiveData;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import androidx.annotation.WorkerThread;
+import androidx.lifecycle.Observer;
 import androidx.slice.Slice;
-import androidx.slice.widget.SliceLiveData;
+import androidx.slice.SliceViewManager;
+import androidx.slice.SliceViewManager.SliceCallback;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 
+import java.util.ArrayList;
 import java.util.HashMap;
 
 /**
  * Manages Lifecycle for Live search results
  */
-public class LiveSearchManager {
+public class LiveSearchManager implements StateListener<LauncherState> {
+
+    private static final String TAG = "LiveSearchManager";
 
     public static final int SEARCH_APPWIDGET_HOST_ID = 2048;
 
     private final Launcher mLauncher;
-    private final AppWidgetManager mAppWidgetManger;
+    private final HashMap<Uri, SliceLifeCycle> mUriSliceMap = new HashMap<>();
+
     private final HashMap<ComponentKey, SearchWidgetInfoContainer> mWidgetPlaceholders =
             new HashMap<>();
-    private final HashMap<Uri, LiveData<Slice>> mUriSliceMap = new HashMap<>();
     private SearchWidgetHost mSearchWidgetHost;
 
     public LiveSearchManager(Launcher launcher) {
         mLauncher = launcher;
-        mAppWidgetManger = AppWidgetManager.getInstance(launcher);
+        mLauncher.getStateManager().addStateListener(this);
     }
 
     /**
@@ -63,77 +80,80 @@
      */
     public SearchWidgetInfoContainer getPlaceHolderWidget(AppWidgetProviderInfo providerInfo) {
         if (mSearchWidgetHost == null) {
-            throw new RuntimeException("AppWidgetHost has not been created yet");
+            mSearchWidgetHost = new SearchWidgetHost(mLauncher);
+            mSearchWidgetHost.startListening();
         }
 
         ComponentName provider = providerInfo.provider;
         UserHandle userHandle = providerInfo.getProfile();
 
         ComponentKey key = new ComponentKey(provider, userHandle);
-        SearchWidgetInfoContainer view = mWidgetPlaceholders.getOrDefault(key, null);
         if (mWidgetPlaceholders.containsKey(key)) {
             return mWidgetPlaceholders.get(key);
         }
+
         LauncherAppWidgetProviderInfo pinfo = LauncherAppWidgetProviderInfo.fromProviderInfo(
                 mLauncher, providerInfo);
         PendingAddWidgetInfo pendingAddWidgetInfo = new PendingAddWidgetInfo(pinfo);
 
         Bundle options = getDefaultOptionsForWidget(mLauncher, pendingAddWidgetInfo);
         int appWidgetId = mSearchWidgetHost.allocateAppWidgetId();
-        boolean success = mAppWidgetManger.bindAppWidgetIdIfAllowed(appWidgetId, userHandle,
-                provider, options);
+        boolean success = AppWidgetManager.getInstance(mLauncher)
+                .bindAppWidgetIdIfAllowed(appWidgetId, userHandle, provider, options);
         if (!success) {
+            mSearchWidgetHost.deleteAppWidgetId(appWidgetId);
             mWidgetPlaceholders.put(key, null);
             return null;
         }
 
-        view = (SearchWidgetInfoContainer) mSearchWidgetHost.createView(mLauncher, appWidgetId,
-                providerInfo);
+        SearchWidgetInfoContainer view = (SearchWidgetInfoContainer) mSearchWidgetHost.createView(
+                mLauncher, appWidgetId, providerInfo);
         view.setTag(pendingAddWidgetInfo);
         mWidgetPlaceholders.put(key, view);
         return view;
     }
 
     /**
-     * Creates {@link LiveData<Slice>} from Slice Uri. Caches created live data to be reused
-     * within the same search session. Removes previous observers when new SliceView request a
-     * live data for observation.
-     */
-    public LiveData<Slice> getSliceForUri(Uri sliceUri) {
-        LiveData<Slice> sliceLiveData = mUriSliceMap.getOrDefault(sliceUri, null);
-        if (sliceLiveData == null) {
-            sliceLiveData = SliceLiveData.fromUri(mLauncher, sliceUri);
-            mUriSliceMap.put(sliceUri, sliceLiveData);
-        }
-        return sliceLiveData;
-    }
-
-    /**
-     * Start search session
-     */
-    public void start() {
-        stop();
-        mSearchWidgetHost = new SearchWidgetHost(mLauncher);
-        mSearchWidgetHost.startListening();
-    }
-
-    /**
      * Stop search session
      */
     public void stop() {
+        clearWidgetHost();
+    }
+
+    private void clearWidgetHost() {
         if (mSearchWidgetHost != null) {
             mSearchWidgetHost.stopListening();
+            mSearchWidgetHost.clearViews();
             mSearchWidgetHost.deleteHost();
-            for (SearchWidgetInfoContainer placeholder : mWidgetPlaceholders.values()) {
-                placeholder.clearListeners();
-            }
             mWidgetPlaceholders.clear();
             mSearchWidgetHost = null;
         }
-        for (LiveData<Slice> liveData : mUriSliceMap.values()) {
-            liveData.removeObservers(mLauncher);
+    }
+
+    @Override
+    public void onStateTransitionComplete(LauncherState finalState) {
+        if (finalState != ALL_APPS) {
+            // Clear all search session related objects
+            mUriSliceMap.values().forEach(SliceLifeCycle::destroy);
+            mUriSliceMap.clear();
+
+            clearWidgetHost();
         }
-        mUriSliceMap.clear();
+    }
+
+    /**
+     * Adds a new observer for the provided uri and returns a callback to cancel this observer
+     */
+    public SafeCloseable addObserver(Uri uri, Observer<Slice> listener) {
+        SliceLifeCycle slc = mUriSliceMap.get(uri);
+        if (slc == null) {
+            slc = new SliceLifeCycle(uri, mLauncher);
+            mUriSliceMap.put(uri, slc);
+        }
+        slc.addListener(listener);
+
+        final SliceLifeCycle sliceLifeCycle = slc;
+        return () -> sliceLifeCycle.removeListener(listener);
     }
 
     static class SearchWidgetHost extends AppWidgetHost {
@@ -146,5 +166,121 @@
                 AppWidgetProviderInfo appWidget) {
             return new SearchWidgetInfoContainer(context);
         }
+
+        @Override
+        public void clearViews() {
+            super.clearViews();
+        }
+    }
+
+    private static class SliceLifeCycle
+            implements ActivityLifecycleCallbacks, SliceCallback {
+
+        private final Uri mUri;
+        private final Launcher mLauncher;
+        private final SliceViewManager mSliceViewManager;
+        private final ArrayList<Observer<Slice>> mListeners = new ArrayList<>();
+
+        private boolean mDestroyed = false;
+        private boolean mWasListening = false;
+
+        SliceLifeCycle(Uri uri, Launcher launcher) {
+            mUri = uri;
+            mLauncher = launcher;
+            mSliceViewManager = SliceViewManager.getInstance(launcher);
+            launcher.registerActivityLifecycleCallbacks(this);
+
+            if (launcher.isDestroyed()) {
+                onActivityDestroyed(launcher);
+            } else if (launcher.isStarted()) {
+                onActivityStarted(launcher);
+            }
+        }
+
+        @Override
+        public void onActivityDestroyed(Activity activity) {
+            destroy();
+        }
+
+        @Override
+        public void onActivityStarted(Activity activity) {
+            updateListening();
+        }
+
+        @Override
+        public void onActivityStopped(Activity activity) {
+            updateListening();
+        }
+
+        private void updateListening() {
+            boolean isListening = mDestroyed
+                    ? false
+                    : (mLauncher.isStarted() && !mListeners.isEmpty());
+            UI_HELPER_EXECUTOR.execute(() -> uploadListeningBg(isListening));
+        }
+
+        @WorkerThread
+        private void uploadListeningBg(boolean isListening) {
+            if (mWasListening != isListening) {
+                mWasListening = isListening;
+                if (isListening) {
+                    mSliceViewManager.registerSliceCallback(mUri, MAIN_EXECUTOR, this);
+                    // Update slice one-time on the different thread so that we can display
+                    // multiple slices in parallel
+                    THREAD_POOL_EXECUTOR.execute(this::updateSlice);
+                } else {
+                    mSliceViewManager.unregisterSliceCallback(mUri, this);
+                }
+            }
+        }
+
+        @UiThread
+        private void addListener(Observer<Slice> listener) {
+            mListeners.add(listener);
+            updateListening();
+        }
+
+        @UiThread
+        private void removeListener(Observer<Slice> listener) {
+            mListeners.remove(listener);
+            updateListening();
+        }
+
+        @WorkerThread
+        private void updateSlice() {
+            try {
+                Slice s = mSliceViewManager.bindSlice(mUri);
+                MAIN_EXECUTOR.execute(() -> onSliceUpdated(s));
+            } catch (Exception e) {
+                Log.d(TAG, "Error fetching slice", e);
+            }
+        }
+
+        @UiThread
+        @Override
+        public void onSliceUpdated(@Nullable Slice s) {
+            mListeners.forEach(l -> l.onChanged(s));
+        }
+
+        private void destroy() {
+            if (mDestroyed) {
+                return;
+            }
+            mDestroyed = true;
+            mLauncher.unregisterActivityLifecycleCallbacks(this);
+            mListeners.clear();
+        }
+
+        @Override
+        public void onActivityCreated(Activity activity, Bundle bundle) { }
+
+        @Override
+        public void onActivityPaused(Activity activity) { }
+
+        @Override
+        public void onActivityResumed(Activity activity) { }
+
+        @Override
+        public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { }
     }
 }
diff --git a/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java b/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java
index a79ec43..fdacd3d 100644
--- a/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java
+++ b/src/com/android/launcher3/allapps/search/SearchAdapterProvider.java
@@ -51,10 +51,17 @@
             ViewGroup parent, int viewType);
 
     /**
+     * Returns supported item per row combinations supported
+     */
+    public int[] getSupportedItemsPerRowArray() {
+        return new int[]{};
+    }
+
+    /**
      * Returns how many cells a view should span
      */
-    public int getGridSpanSize(int viewType, int appsPerRow) {
-        return appsPerRow * AllAppsGridAdapter.SPAN_MULTIPLIER;
+    public int getItemsPerRow(int viewType, int appsPerRow) {
+        return appsPerRow;
     }
 
     /**
diff --git a/src/com/android/launcher3/allapps/search/SearchWidgetInfoContainer.java b/src/com/android/launcher3/allapps/search/SearchWidgetInfoContainer.java
index b5c2268..8e5f8cb 100644
--- a/src/com/android/launcher3/allapps/search/SearchWidgetInfoContainer.java
+++ b/src/com/android/launcher3/allapps/search/SearchWidgetInfoContainer.java
@@ -70,10 +70,4 @@
         mListeners.remove(hostView);
     }
 
-    /**
-     * Removes all AppWidgetHost update listeners
-     */
-    public void clearListeners() {
-        mListeners.clear();
-    }
 }
diff --git a/src/com/android/launcher3/allapps/search/SearchSectionInfo.java b/src/com/android/launcher3/allapps/search/SectionDecorationInfo.java
similarity index 73%
rename from src/com/android/launcher3/allapps/search/SearchSectionInfo.java
rename to src/com/android/launcher3/allapps/search/SectionDecorationInfo.java
index 464df68..0b64fca 100644
--- a/src/com/android/launcher3/allapps/search/SearchSectionInfo.java
+++ b/src/com/android/launcher3/allapps/search/SectionDecorationInfo.java
@@ -18,37 +18,30 @@
 import com.android.launcher3.allapps.AllAppsSectionDecorator.SectionDecorationHandler;
 
 /**
- * Info class for a search section
+ * Info class for a search section that is primarily used for decoration.
  */
-public class SearchSectionInfo {
+public class SectionDecorationInfo {
+
+    public static final int QUICK_LAUNCH = 1 << 0;
+    public static final int GROUPING = 1 << 1;
 
     private String mSectionId;
+    private boolean mFocused;
     private SectionDecorationHandler mDecorationHandler;
 
-    public int getPosStart() {
-        return mPosStart;
+    public boolean isFocusedView() {
+        return mFocused;
     }
 
-    public void setPosStart(int posStart) {
-        mPosStart = posStart;
+    public void setFocusedView(boolean focused) {
+        mFocused = focused;
     }
 
-    public int getPosEnd() {
-        return mPosEnd;
-    }
-
-    public void setPosEnd(int posEnd) {
-        mPosEnd = posEnd;
-    }
-
-    private int mPosStart;
-    private int mPosEnd;
-
-    public SearchSectionInfo() {
+    public SectionDecorationInfo() {
         this(null);
     }
 
-    public SearchSectionInfo(String sectionId) {
+    public SectionDecorationInfo(String sectionId) {
         mSectionId = sectionId;
     }
 
@@ -56,7 +49,6 @@
         mDecorationHandler = sectionDecorationHandler;
     }
 
-
     public SectionDecorationHandler getDecorationHandler() {
         return mDecorationHandler;
     }
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index 8016b2d..6b9ed09 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -16,9 +16,6 @@
 
 package com.android.launcher3.anim;
 
-import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
-
-import android.content.Context;
 import android.graphics.Path;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.animation.AccelerateInterpolator;
@@ -67,9 +64,6 @@
      */
     public static final Interpolator FINAL_FRAME = t -> t < 1 ? 0 : 1;
 
-    private static final int MIN_SETTLE_DURATION = 200;
-    private static final float OVERSHOOT_FACTOR = 0.9f;
-
     static {
         Path exaggeratedEase = new Path();
         exaggeratedEase.moveTo(0, 0);
@@ -175,76 +169,4 @@
             float upperBound) {
         return t -> Utilities.mapRange(interpolator.getInterpolation(t), lowerBound, upperBound);
     }
-
-    /**
-     * Computes parameters necessary for an overshoot effect.
-     */
-    public static class OvershootParams {
-        public Interpolator interpolator;
-        public float start;
-        public float end;
-        public long duration;
-
-        /**
-         * Given the input params, sets OvershootParams variables to be used by the caller.
-         * @param startProgress The progress from 0 to 1 that the overshoot starts from.
-         * @param overshootPastProgress The progress from 0 to 1 where we overshoot past (should
-         *        either be equal to startProgress or endProgress, depending on if we want to
-         *        overshoot immediately or only once we reach the end).
-         * @param endProgress The final progress from 0 to 1 that we will settle to.
-         * @param velocityPxPerMs The initial velocity that causes this overshoot.
-         * @param totalDistancePx The distance against which progress is calculated.
-         */
-        public OvershootParams(float startProgress, float overshootPastProgress,
-                float endProgress, float velocityPxPerMs, int totalDistancePx, Context context) {
-            velocityPxPerMs = Math.abs(velocityPxPerMs);
-            overshootPastProgress = Math.max(overshootPastProgress, startProgress);
-            start = startProgress;
-            int startPx = (int) (start * totalDistancePx);
-            // Overshoot by about half a frame.
-            float overshootBy = OVERSHOOT_FACTOR * velocityPxPerMs *
-                    getSingleFrameMs(context) / totalDistancePx / 2;
-            overshootBy = Utilities.boundToRange(overshootBy, 0.02f, 0.15f);
-            end = overshootPastProgress + overshootBy;
-            int endPx = (int) (end  * totalDistancePx);
-            int overshootDistance = endPx - startPx;
-            // Calculate deceleration necessary to reach overshoot distance.
-            // Formula: velocityFinal^2 = velocityInitial^2 + 2 * acceleration * distance
-            //          0 = v^2 + 2ad (velocityFinal == 0)
-            //          a = v^2 / -2d
-            float decelerationPxPerMs = velocityPxPerMs * velocityPxPerMs / (2 * overshootDistance);
-            // Calculate time necessary to reach peak of overshoot.
-            // Formula: acceleration = velocity / time
-            //          time = velocity / acceleration
-            duration = (long) (velocityPxPerMs / decelerationPxPerMs);
-
-            // Now that we're at the top of the overshoot, need to settle back to endProgress.
-            float settleDistance = end - endProgress;
-            int settleDistancePx = (int) (settleDistance * totalDistancePx);
-            // Calculate time necessary for the settle.
-            // Formula: distance = velocityInitial * time + 1/2 * acceleration * time^2
-            //          d = 1/2at^2 (velocityInitial = 0, since we just stopped at the top)
-            //          t = sqrt(2d/a)
-            // Above formula assumes constant acceleration. Since we use ACCEL_DEACCEL, we actually
-            // have acceleration to halfway then deceleration the rest. So the formula becomes:
-            //          t = sqrt(d/a) * 2 (half the distance for accel, half for deaccel)
-            long settleDuration = (long) Math.sqrt(settleDistancePx / decelerationPxPerMs) * 4;
-
-            settleDuration = Math.max(MIN_SETTLE_DURATION, settleDuration);
-            // How much of the animation to devote to playing the overshoot (the rest is for settle).
-            float overshootFraction = (float) duration / (duration + settleDuration);
-            duration += settleDuration;
-            // Finally, create the interpolator, composed of two interpolators: an overshoot, which
-            // reaches end > 1, and then a settle to endProgress.
-            Interpolator overshoot = Interpolators.clampToProgress(DEACCEL, 0, overshootFraction);
-            // The settle starts at 1, where 1 is the top of the overshoot, and maps to a fraction
-            // such that final progress is endProgress. For example, if we overshot to 1.1 but want
-            // to end at 1, we need to map to 1/1.1.
-            Interpolator settle = Interpolators.clampToProgress(Interpolators.mapToProgress(
-                    ACCEL_DEACCEL, 1, (endProgress - start) / (end - start)), overshootFraction, 1);
-            interpolator = t -> t <= overshootFraction
-                    ? overshoot.getInterpolation(t)
-                    : settle.getInterpolation(t);
-        }
-    }
 }
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 086d665..e406e9b 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -72,9 +72,6 @@
             "PROMISE_APPS_NEW_INSTALLS", true,
             "Adds a promise icon to the home screen for new install sessions.");
 
-    public static final BooleanFlag APPLY_CONFIG_AT_RUNTIME = getDebugFlag(
-            "APPLY_CONFIG_AT_RUNTIME", true, "Apply display changes dynamically");
-
     public static final BooleanFlag QUICKSTEP_SPRINGS = getDebugFlag(
             "QUICKSTEP_SPRINGS", true, "Enable springs for quickstep animations");
 
@@ -98,12 +95,6 @@
     public static final BooleanFlag ENABLE_DEVICE_SEARCH = new DeviceFlag(
             "ENABLE_DEVICE_SEARCH", false, "Allows on device search in all apps");
 
-    public static final BooleanFlag USE_SEARCH_API = getDebugFlag(
-            "USE_SEARCH_API", true, "Use SearchUIManager api for device search");
-
-    public static final BooleanFlag DISABLE_INITIAL_IME_IN_ALLAPPS = getDebugFlag(
-            "DISABLE_INITIAL_IME_IN_ALLAPPS", false, "Disable default IME state in all apps");
-
     public static final BooleanFlag FOLDER_NAME_SUGGEST = new DeviceFlag(
             "FOLDER_NAME_SUGGEST", true,
             "Suggests folder names instead of blank text.");
@@ -133,9 +124,6 @@
             "ASSISTANT_GIVES_LAUNCHER_FOCUS", false,
             "Allow Launcher to handle nav bar gestures while Assistant is running over it");
 
-    public static final BooleanFlag ENABLE_HYBRID_HOTSEAT = getDebugFlag(
-            "ENABLE_HYBRID_HOTSEAT", true, "Fill gaps in hotseat with predicted apps");
-
     public static final BooleanFlag HOTSEAT_MIGRATE_TO_FOLDER = getDebugFlag(
             "HOTSEAT_MIGRATE_TO_FOLDER", false, "Should move hotseat items into a folder");
 
@@ -170,15 +158,11 @@
             "ENABLE_SMARTSPACE_UNIVERSAL", false,
             "Replace Smartspace with a version rendered by System UI.");
 
-    public static final BooleanFlag ENABLE_SMARTSPACE_ENHANCED = getDebugFlag(
+    public static final BooleanFlag ENABLE_SMARTSPACE_ENHANCED = new DeviceFlag(
             "ENABLE_SMARTSPACE_ENHANCED", false,
             "Replace Smartspace with the enhanced version. "
               + "Ignored if ENABLE_SMARTSPACE_UNIVERSAL is enabled.");
 
-    public static final BooleanFlag ENABLE_SYSTEM_VELOCITY_PROVIDER = getDebugFlag(
-            "ENABLE_SYSTEM_VELOCITY_PROVIDER", true,
-            "Use system VelocityTracker's algorithm for motion pause detection.");
-
     public static final BooleanFlag ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS =
             getDebugFlag(
             "ALWAYS_USE_HARDWARE_OPTIMIZATION_FOR_FOLDER_ANIMATIONS", false,
@@ -217,6 +201,10 @@
     public static final BooleanFlag ENABLE_TASKBAR = new DeviceFlag(
             "ENABLE_TASKBAR", false, "Allows a system Taskbar to be shown on larger devices.");
 
+    public static final BooleanFlag ENABLE_OVERVIEW_GRID = new DeviceFlag(
+            "ENABLE_OVERVIEW_GRID", false, "Uses grid overview layout. "
+            + "Only applicable on large screen devices.");
+
     public static void initialize(Context context) {
         synchronized (sDebugFlags) {
             for (DebugFlag flag : sDebugFlags) {
@@ -275,6 +263,8 @@
         }
 
         public void addChangeListener(Context context, Runnable r) { }
+
+        public void removeChangeListener(Runnable r) {}
     }
 
     public static class DebugFlag extends BooleanFlag {
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index 77d2b85..c972cbb 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -47,8 +47,6 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetHost;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.ItemInstallQueue;
@@ -56,8 +54,11 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.pm.PinRequestHelper;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
+import com.android.launcher3.widget.WidgetCell;
 import com.android.launcher3.widget.WidgetHostViewLoader;
 import com.android.launcher3.widget.WidgetImageView;
 import com.android.launcher3.widget.WidgetManagerHelper;
@@ -78,7 +79,7 @@
     private LauncherAppState mApp;
     private InvariantDeviceProfile mIdp;
 
-    private LivePreviewWidgetCell mWidgetCell;
+    private WidgetCell mWidgetCell;
 
     // Widget request specific options.
     private LauncherAppWidgetHost mAppWidgetHost;
@@ -117,8 +118,9 @@
             }
         }
 
-        mWidgetCell.setOnTouchListener(this);
-        mWidgetCell.setOnLongClickListener(this);
+        WidgetImageView preview = mWidgetCell.findViewById(R.id.widget_preview);
+        preview.setOnTouchListener(this);
+        preview.setOnLongClickListener(this);
 
         // savedInstanceState is null when the activity is created the first time (i.e., avoids
         // duplicate logging during rotation)
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 1cfe6ac..93df599 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -40,12 +40,14 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
+import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.TouchController;
 
 import java.util.ArrayList;
+import java.util.Optional;
 
 /**
  * Class for initiating a drag within a view or across multiple views.
@@ -230,6 +232,11 @@
         }
     }
 
+    public Optional<InstanceId> getLogInstanceId() {
+        return Optional.ofNullable(mDragObject)
+                .map(dragObject -> dragObject.logInstanceId);
+    }
+
     /**
      * Call this from a drag source view like this:
      *
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index ddf44ca..7a6b4f9 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -41,12 +41,14 @@
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DropTargetBar;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherRootView;
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.graphics.OverviewScrim;
-import com.android.launcher3.graphics.WorkspaceAndHotseatScrim;
+import com.android.launcher3.graphics.SysUiScrim;
+import com.android.launcher3.graphics.WorkspaceDragScrim;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.views.BaseDragLayer;
@@ -82,11 +84,10 @@
 
     // Related to adjacent page hints
     private final ViewGroupFocusHelper mFocusIndicatorHelper;
-    private final WorkspaceAndHotseatScrim mWorkspaceScrim;
     private final OverviewScrim mOverviewScrim;
-
-    // View that should handle move events
-    private View mMoveTarget;
+    private WorkspaceDragScrim mWorkspaceDragScrim;
+    private SysUiScrim mSysUiScrim;
+    private LauncherRootView mRootView;
 
     /**
      * Used to create a new DragLayer from XML.
@@ -102,15 +103,23 @@
         setChildrenDrawingOrderEnabled(true);
 
         mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
-        mWorkspaceScrim = new WorkspaceAndHotseatScrim(this);
         mOverviewScrim = new OverviewScrim(this);
     }
 
     public void setup(DragController dragController, Workspace workspace) {
         mDragController = dragController;
-        mWorkspaceScrim.setWorkspace(workspace);
-        mMoveTarget = workspace;
         recreateControllers();
+
+        mWorkspaceDragScrim = new WorkspaceDragScrim((this));
+        mWorkspaceDragScrim.setWorkspace(workspace);
+
+        // We delegate drawing of the workspace scrim to LauncherRootView (one level up), so as
+        // to avoid artifacts when translating the entire drag layer in the -1 transition.
+        mRootView = (LauncherRootView) getParent();
+        mSysUiScrim = new SysUiScrim(mRootView);
+        mRootView.setSysUiScrim(mSysUiScrim);
+
+
     }
 
     @Override
@@ -215,12 +224,6 @@
     }
 
     @Override
-    public boolean dispatchUnhandledMove(View focused, int direction) {
-        return super.dispatchUnhandledMove(focused, direction)
-                || mMoveTarget.dispatchUnhandledMove(focused, direction);
-    }
-
-    @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
         ev.offsetLocation(getTranslationX(), 0);
         try {
@@ -525,7 +528,7 @@
     @Override
     protected void dispatchDraw(Canvas canvas) {
         // Draw the background below children.
-        mWorkspaceScrim.draw(canvas);
+        mWorkspaceDragScrim.draw(canvas);
         mOverviewScrim.updateCurrentScrimmedView(this);
         mFocusIndicatorHelper.draw(canvas);
         super.dispatchDraw(canvas);
@@ -545,18 +548,22 @@
     @Override
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
         super.onSizeChanged(w, h, oldw, oldh);
-        mWorkspaceScrim.setSize(w, h);
+        mSysUiScrim.setSize(w, h);
     }
 
     @Override
     public void setInsets(Rect insets) {
         super.setInsets(insets);
-        mWorkspaceScrim.onInsetsChanged(insets, mAllowSysuiScrims);
+        mSysUiScrim.onInsetsChanged(insets, mAllowSysuiScrims);
         mOverviewScrim.onInsetsChanged(insets);
     }
 
-    public WorkspaceAndHotseatScrim getScrim() {
-        return mWorkspaceScrim;
+    public WorkspaceDragScrim getWorkspaceDragScrim() {
+        return mWorkspaceDragScrim;
+    }
+
+    public SysUiScrim getSysUiScrim() {
+        return mSysUiScrim;
     }
 
     public OverviewScrim getOverviewScrim() {
diff --git a/src/com/android/launcher3/dragndrop/DragOptions.java b/src/com/android/launcher3/dragndrop/DragOptions.java
index 959602b..e8ff8da 100644
--- a/src/com/android/launcher3/dragndrop/DragOptions.java
+++ b/src/com/android/launcher3/dragndrop/DragOptions.java
@@ -28,6 +28,9 @@
     /** Whether or not an accessible drag operation is in progress. */
     public boolean isAccessibleDrag = false;
 
+    /** Whether or not the drag operation is controlled by keyboard. */
+    public boolean isKeyboardDrag = false;
+
     /**
      * Specifies the start location for a simulated DnD (like system drag or accessibility drag),
      * null when using internal DnD
diff --git a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
index 7788f93..0a1aba1 100644
--- a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
+++ b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
@@ -65,6 +65,9 @@
     }
 
     public Runnable getFlingAnimation(DropTarget.DragObject dragObject, DragOptions options) {
+        if (options == null) {
+            return null;
+        }
         PointF vel = isFlingingToDelete();
         options.isFlingToDelete = vel != null;
         if (!options.isFlingToDelete) {
diff --git a/src/com/android/launcher3/dragndrop/LivePreviewWidgetCell.java b/src/com/android/launcher3/dragndrop/LivePreviewWidgetCell.java
deleted file mode 100644
index a9389bc..0000000
--- a/src/com/android/launcher3/dragndrop/LivePreviewWidgetCell.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package com.android.launcher3.dragndrop;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.RemoteViews;
-
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
-import com.android.launcher3.icons.BitmapRenderer;
-import com.android.launcher3.widget.WidgetCell;
-
-/**
- * Extension of {@link WidgetCell} which supports generating previews from {@link RemoteViews}
- */
-public class LivePreviewWidgetCell extends WidgetCell {
-
-    private RemoteViews mPreview;
-
-    public LivePreviewWidgetCell(Context context) {
-        this(context, null);
-    }
-
-    public LivePreviewWidgetCell(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public LivePreviewWidgetCell(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-    }
-
-    public void setPreview(RemoteViews view) {
-        mPreview = view;
-    }
-
-    @Override
-    public void ensurePreview() {
-        if (mPreview != null && mActiveRequest == null) {
-            Bitmap preview = generateFromRemoteViews(
-                    mActivity, mPreview, mItem.widgetInfo, mPresetPreviewSize, new int[1]);
-            if (preview != null) {
-                applyPreview(preview);
-                return;
-            }
-        }
-        super.ensurePreview();
-    }
-
-    /**
-     * Generates a bitmap by inflating {@param views}.
-     * @see com.android.launcher3.WidgetPreviewLoader#generateWidgetPreview
-     *
-     * TODO: Consider moving this to the background thread.
-     */
-    public static Bitmap generateFromRemoteViews(BaseActivity activity, RemoteViews views,
-            LauncherAppWidgetProviderInfo info, int previewSize, int[] preScaledWidthOut) {
-
-        DeviceProfile dp = activity.getDeviceProfile();
-        int viewWidth = dp.cellWidthPx * info.spanX;
-        int viewHeight = dp.cellHeightPx * info.spanY;
-
-        final View v;
-        try {
-            v = views.apply(activity, new FrameLayout(activity));
-            v.measure(MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY),
-                    MeasureSpec.makeMeasureSpec(viewHeight, MeasureSpec.EXACTLY));
-
-            viewWidth = v.getMeasuredWidth();
-            viewHeight = v.getMeasuredHeight();
-            v.layout(0, 0, viewWidth, viewHeight);
-        } catch (Exception e) {
-            return null;
-        }
-
-        preScaledWidthOut[0] = viewWidth;
-        final int bitmapWidth, bitmapHeight;
-        final float scale;
-        if (viewWidth > previewSize) {
-            scale = ((float) previewSize) / viewWidth;
-            bitmapWidth = previewSize;
-            bitmapHeight = (int) (viewHeight * scale);
-        } else {
-            scale = 1;
-            bitmapWidth = viewWidth;
-            bitmapHeight = viewHeight;
-        }
-
-        return BitmapRenderer.createSoftwareBitmap(bitmapWidth, bitmapHeight, c -> {
-            c.scale(scale, scale);
-            v.draw(c);
-        });
-    }
-}
diff --git a/src/com/android/launcher3/dragndrop/PinItemDragListener.java b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
index 6104d80..2290473 100644
--- a/src/com/android/launcher3/dragndrop/PinItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
@@ -30,8 +30,8 @@
 
 import com.android.launcher3.DragSource;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.PendingAddItemInfo;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.PendingItemDragHelper;
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 61938d1..bcb3a54 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -44,8 +44,10 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Pair;
+import android.util.TypedValue;
 import android.view.FocusFinder;
 import android.view.KeyEvent;
+import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewDebug;
@@ -82,7 +84,6 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragController.DragListener;
-import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.logger.LauncherAtom.FromState;
 import com.android.launcher3.logger.LauncherAtom.ToState;
@@ -96,6 +97,8 @@
 import com.android.launcher3.pageindicators.PageIndicatorDots;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.BaseDragLayer;
 import com.android.launcher3.views.ClipPathView;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 
@@ -165,7 +168,11 @@
     private AnimatorSet mCurrentAnimator;
     private boolean mIsAnimatingClosed = false;
 
+    // Folder can be displayed in Launcher's activity or a separate window (e.g. Taskbar).
+    // Anything specific to Launcher should use mLauncher, otherwise should use mActivityContext.
     protected final Launcher mLauncher;
+    protected final ActivityContext mActivityContext;
+
     protected DragController mDragController;
     public FolderInfo mInfo;
     private CharSequence mFromTitle;
@@ -228,6 +235,7 @@
         setAlwaysDrawnWithCacheEnabled(false);
 
         mLauncher = Launcher.getLauncher(context);
+        mActivityContext = ActivityContext.lookupContext(context);
         mStatsLogManager = StatsLogManager.newInstance(context);
         // We need this view to be focusable in touch mode so that when text editing of the folder
         // name is complete, we have something to focus on, thus hiding the cursor and giving
@@ -239,11 +247,16 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
+        final DeviceProfile dp = mLauncher.getDeviceProfile();
+        final int paddingLeftRight = dp.folderContentPaddingLeftRight;
+
         mContent = findViewById(R.id.folder_content);
+        mContent.setPadding(paddingLeftRight, dp.folderContentPaddingTop, paddingLeftRight, 0);
         mContent.setFolder(this);
 
         mPageIndicator = findViewById(R.id.folder_page_indicator);
         mFolderName = findViewById(R.id.folder_name);
+        mFolderName.setTextSize(TypedValue.COMPLEX_UNIT_PX, dp.folderLabelTextSizePx);
         mFolderName.setOnBackKeyListener(this);
         mFolderName.setOnFocusChangeListener(this);
         mFolderName.setOnEditorActionListener(this);
@@ -255,12 +268,7 @@
         mFolderName.forceDisableSuggestions(true);
 
         mFooter = findViewById(R.id.folder_footer);
-
-        // We find out how tall footer wants to be (it is set to wrap_content), so that
-        // we can allocate the appropriate amount of space for it.
-        int measureSpec = MeasureSpec.UNSPECIFIED;
-        mFooter.measure(measureSpec, measureSpec);
-        mFooterHeight = mFooter.getMeasuredHeight();
+        mFooterHeight = getResources().getDimensionPixelSize(R.dimen.folder_label_height);
 
         if (Utilities.ATLEAST_R) {
             mFolderWindowInsetsAnimationCallback =
@@ -457,9 +465,9 @@
         Collections.sort(children, ITEM_POS_COMPARATOR);
         updateItemLocationsInDatabaseBatch(true);
 
-        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
+        BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) getLayoutParams();
         if (lp == null) {
-            lp = new DragLayer.LayoutParams(0, 0);
+            lp = new BaseDragLayer.LayoutParams(0, 0);
             lp.customPosition = true;
             setLayoutParams(lp);
         }
@@ -513,13 +521,14 @@
     /**
      * Creates a new UserFolder, inflated from R.layout.user_folder.
      *
-     * @param launcher The main activity.
+     * @param activityContext The main ActivityContext in which to inflate this Folder. It must also
+     *                        be an instance or ContextWrapper around the Launcher activity context.
      *
      * @return A new UserFolder.
      */
     @SuppressLint("InflateParams")
-    static Folder fromXml(Launcher launcher) {
-        return (Folder) launcher.getLayoutInflater()
+    static <T extends Context & ActivityContext> Folder fromXml(T activityContext) {
+        return (Folder) LayoutInflater.from(activityContext).cloneInContext(activityContext)
                 .inflate(R.layout.user_folder_icon_normalized, null);
     }
 
@@ -597,7 +606,7 @@
      * is played.
      */
     private void animateOpen(List<WorkspaceItemInfo> items, int pageNo) {
-        Folder openFolder = getOpen(mLauncher);
+        Folder openFolder = getOpen(mActivityContext);
         if (openFolder != null && openFolder != this) {
             // Close any open folder before opening a folder.
             openFolder.close(true);
@@ -610,7 +619,7 @@
 
         mIsOpen = true;
 
-        DragLayer dragLayer = mLauncher.getDragLayer();
+        BaseDragLayer dragLayer = mActivityContext.getDragLayer();
         // Just verify that the folder hasn't already been added to the DragLayer.
         // There was a one-off crash where the folder had a parent already.
         if (getParent() == null) {
@@ -724,7 +733,7 @@
 
         // Notify the accessibility manager that this folder "window" has disappeared and no
         // longer occludes the workspace items
-        mLauncher.getDragLayer().sendAccessibilityEvent(
+        mActivityContext.getDragLayer().sendAccessibilityEvent(
                 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
     }
 
@@ -772,7 +781,7 @@
 
     private void closeComplete(boolean wasAnimated) {
         // TODO: Clear all active animations.
-        DragLayer parent = (DragLayer) getParent();
+        BaseDragLayer parent = (BaseDragLayer) getParent();
         if (parent != null) {
             parent.removeView(this);
         }
@@ -1011,7 +1020,7 @@
 
     private void updateItemLocationsInDatabaseBatch(boolean isBind) {
         FolderGridOrganizer verifier = new FolderGridOrganizer(
-                mLauncher.getDeviceProfile().inv).setFolderInfo(mInfo);
+                mActivityContext.getDeviceProfile().inv).setFolderInfo(mInfo);
 
         ArrayList<ItemInfo> items = new ArrayList<>();
         int total = mInfo.contents.size();
@@ -1048,10 +1057,8 @@
     }
 
     private void centerAboutIcon() {
-        DeviceProfile grid = mLauncher.getDeviceProfile();
-
-        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
-        DragLayer parent = mLauncher.getDragLayer();
+        BaseDragLayer.LayoutParams lp = (BaseDragLayer.LayoutParams) getLayoutParams();
+        BaseDragLayer parent = mActivityContext.getDragLayer();
         int width = getFolderWidth();
         int height = getFolderHeight();
 
@@ -1061,38 +1068,13 @@
         int centeredLeft = centerX - width / 2;
         int centeredTop = centerY - height / 2;
 
-        // We need to bound the folder to the currently visible workspace area
-        if (mLauncher.getStateManager().getState().overviewUi) {
-            parent.getDescendantRectRelativeToSelf(mLauncher.getOverviewPanel(), sTempRect);
-        } else {
-            mLauncher.getWorkspace().getPageAreaRelativeToDragLayer(sTempRect);
-        }
-        int left = Math.min(Math.max(sTempRect.left, centeredLeft),
-                sTempRect.right- width);
-        int top = Math.min(Math.max(sTempRect.top, centeredTop),
-                sTempRect.bottom - height);
-
-        int distFromEdgeOfScreen = mLauncher.getWorkspace().getPaddingLeft() + getPaddingLeft();
-
-        if (grid.isPhone && (grid.availableWidthPx - width) < 4 * distFromEdgeOfScreen) {
-            // Center the folder if it is very close to being centered anyway, by virtue of
-            // filling the majority of the viewport. ie. remove it from the uncanny valley
-            // of centeredness.
-            left = (grid.availableWidthPx - width) / 2;
-        } else if (width >= sTempRect.width()) {
-            // If the folder doesn't fit within the bounds, center it about the desired bounds
-            left = sTempRect.left + (sTempRect.width() - width) / 2;
-        }
-        if (height >= sTempRect.height()) {
-            // Folder height is greater than page height, center on page
-            top = sTempRect.top + (sTempRect.height() - height) / 2;
-        } else {
-            // Folder height is less than page height, so bound it to the absolute open folder
-            // bounds if necessary
-            Rect folderBounds = grid.getAbsoluteOpenFolderBounds();
-            left = Math.max(folderBounds.left, Math.min(left, folderBounds.right - width));
-            top = Math.max(folderBounds.top, Math.min(top, folderBounds.bottom - height));
-        }
+        sTempRect.set(mActivityContext.getFolderBoundingBox());
+        int left = Utilities.boundToRange(centeredLeft, sTempRect.left, sTempRect.right - width);
+        int top = Utilities.boundToRange(centeredTop, sTempRect.top, sTempRect.bottom - height);
+        int[] inOutPosition = new int[] {left, top};
+        mActivityContext.updateOpenFolderPosition(inOutPosition, sTempRect, width, height);
+        left = inOutPosition[0];
+        top = inOutPosition[1];
 
         int folderPivotX = width / 2 + (centeredLeft - left);
         int folderPivotY = height / 2 + (centeredTop - top);
@@ -1106,7 +1088,7 @@
     }
 
     protected int getContentAreaHeight() {
-        DeviceProfile grid = mLauncher.getDeviceProfile();
+        DeviceProfile grid = mActivityContext.getDeviceProfile();
         int maxContentAreaHeight = grid.availableHeightPx - grid.getTotalWorkspacePadding().y
                 - mFooterHeight;
         int height = Math.min(maxContentAreaHeight,
@@ -1208,7 +1190,9 @@
                         newIcon.requestFocus();
                     }
                     if (finalItem != null) {
-                        mStatsLogManager.logger().withItemInfo(finalItem)
+                        StatsLogger logger = mStatsLogManager.logger().withItemInfo(finalItem);
+                        mDragController.getLogInstanceId().map(logger::withInstanceId)
+                                .orElse(logger)
                                 .log(LAUNCHER_FOLDER_CONVERTED_TO_ICON);
                     }
                 }
@@ -1382,7 +1366,7 @@
     @Override
     public void onAdd(WorkspaceItemInfo item, int rank) {
         FolderGridOrganizer verifier = new FolderGridOrganizer(
-                mLauncher.getDeviceProfile().inv).setFolderInfo(mInfo);
+                mActivityContext.getDeviceProfile().inv).setFolderInfo(mInfo);
         verifier.updateRankAndPos(item, rank);
         mLauncher.getModelWriter().addOrMoveItemInDatabase(item, mInfo.id, 0, item.cellX,
                 item.cellY);
@@ -1589,8 +1573,8 @@
     /**
      * Returns a folder which is already open or null
      */
-    public static Folder getOpen(Launcher launcher) {
-        return getOpenView(launcher, TYPE_FOLDER);
+    public static Folder getOpen(ActivityContext activityContext) {
+        return getOpenView(activityContext, TYPE_FOLDER);
     }
 
     /**
@@ -1609,7 +1593,7 @@
     @Override
     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            DragLayer dl = mLauncher.getDragLayer();
+            BaseDragLayer dl = (BaseDragLayer) getParent();
 
             if (isEditingName()) {
                 if (!dl.isEventOverView(mFolderName, ev)) {
@@ -1633,6 +1617,11 @@
         return false;
     }
 
+    @Override
+    public boolean canInterceptEventsInSystemGestureRegion() {
+        return true;
+    }
+
     /**
      * Alternative to using {@link #getClipToOutline()} as it only works with derivatives of
      * rounded rect.
@@ -1661,9 +1650,9 @@
 
     /** Returns the height of the current folder's bottom edge from the bottom of the screen. */
     private int getHeightFromBottom() {
-        DragLayer.LayoutParams layoutParams = (DragLayer.LayoutParams) getLayoutParams();
+        BaseDragLayer.LayoutParams layoutParams = (BaseDragLayer.LayoutParams) getLayoutParams();
         int folderBottomPx = layoutParams.y + layoutParams.height;
-        int windowBottomPx = mLauncher.getDeviceProfile().heightPx;
+        int windowBottomPx = mActivityContext.getDeviceProfile().heightPx;
 
         return windowBottomPx - folderBottomPx;
     }
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 3d72b49..feb528c 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -39,14 +39,13 @@
 
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PropertyResetListener;
-import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.BaseDragLayer;
 
 import java.util.List;
 
@@ -69,7 +68,6 @@
     private PreviewBackground mPreviewBackground;
 
     private Context mContext;
-    private Launcher mLauncher;
 
     private final boolean mIsOpening;
 
@@ -92,8 +90,7 @@
         mPreviewBackground = mFolderIcon.mBackground;
 
         mContext = folder.getContext();
-        mLauncher = folder.mLauncher;
-        mPreviewVerifier = new FolderGridOrganizer(mLauncher.getDeviceProfile().inv);
+        mPreviewVerifier = new FolderGridOrganizer(folder.mActivityContext.getDeviceProfile().inv);
 
         mIsOpening = isOpening;
 
@@ -114,14 +111,15 @@
      * Prepares the Folder for animating between open / closed states.
      */
     public AnimatorSet getAnimator() {
-        final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) mFolder.getLayoutParams();
+        final BaseDragLayer.LayoutParams lp =
+                (BaseDragLayer.LayoutParams) mFolder.getLayoutParams();
         mFolderIcon.getPreviewItemManager().recomputePreviewDrawingParams();
         ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule();
         final List<BubbleTextView> itemsInPreview = getPreviewIconsOnPage(0);
 
         // Match position of the FolderIcon
         final Rect folderIconPos = new Rect();
-        float scaleRelativeToDragLayer = mLauncher.getDragLayer()
+        float scaleRelativeToDragLayer = mFolder.mActivityContext.getDragLayer()
                 .getDescendantRectRelativeToSelf(mFolderIcon, folderIconPos);
         int scaledRadius = mPreviewBackground.getScaledRadius();
         float initialSize = (scaledRadius * 2) * scaleRelativeToDragLayer;
@@ -326,7 +324,9 @@
 
             final int previewPosX =
                     (int) ((mTmpParams.transX - iconOffsetX + previewItemOffsetX) / folderScale);
-            final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY) / folderScale);
+            final float paddingTop = btv.getPaddingTop() * iconScale;
+            final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY - paddingTop)
+                    / folderScale);
 
             final float xDistance = previewPosX - btvLp.x;
             final float yDistance = previewPosY - btvLp.y;
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index fe310f6..6b02021 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -166,17 +166,19 @@
         mDotParams = new DotRenderer.DrawParams();
     }
 
-    public static FolderIcon inflateFolderAndIcon(int resId, Launcher launcher, ViewGroup group,
-            FolderInfo folderInfo) {
-        Folder folder = Folder.fromXml(launcher);
-        folder.setDragController(launcher.getDragController());
+    public static <T extends Context & ActivityContext> FolderIcon inflateFolderAndIcon(int resId,
+            T activityContext, ViewGroup group, FolderInfo folderInfo) {
+        Folder folder = Folder.fromXml(activityContext);
+        folder.setDragController(folder.mLauncher.getDragController());
 
-        FolderIcon icon = inflateIcon(resId, launcher, group, folderInfo);
+        FolderIcon icon = inflateIcon(resId, activityContext, group, folderInfo);
         folder.setFolderIcon(icon);
         folder.bind(folderInfo);
         icon.setFolder(folder);
 
-        icon.setOnFocusChangeListener(launcher.getFocusHandler());
+        icon.setOnFocusChangeListener(folder.mLauncher.getFocusHandler());
+        icon.mBackground.paddingY = icon.isInHotseat()
+                ? 0 : activityContext.getDeviceProfile().cellYPaddingPx;
         return icon;
     }
 
@@ -199,7 +201,7 @@
         icon.mFolderName.setText(folderInfo.title);
         icon.mFolderName.setCompoundDrawablePadding(0);
         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams();
-        lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx;
+        lp.topMargin = grid.cellYPaddingPx + grid.iconSizePx + grid.iconDrawablePaddingPx;
 
         icon.setTag(folderInfo);
         icon.setOnClickListener(ItemClickHandler.INSTANCE);
@@ -218,6 +220,7 @@
 
         icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
 
+        icon.mBackground.paddingY = icon.isInHotseat() ? 0 : grid.cellYPaddingPx;
         icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile().inv);
         icon.mPreviewVerifier.setFolderInfo(folderInfo);
         icon.updatePreviewItems(false);
@@ -579,6 +582,7 @@
     public void setFolderBackground(PreviewBackground bg) {
         mBackground = bg;
         mBackground.setInvalidateDelegate(this);
+        mBackground.paddingY = isInHotseat() ? 0 : mActivity.getDeviceProfile().cellYPaddingPx;
     }
 
     @Override
@@ -745,21 +749,19 @@
         mInfo.removeListener(mFolder);
     }
 
+    private boolean isInHotseat() {
+        return mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+    }
+
     public void clearLeaveBehindIfExists() {
-        ((CellLayout.LayoutParams) getLayoutParams()).canReorder = true;
-        if (mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
-            CellLayout cl = (CellLayout) getParent().getParent();
-            cl.clearFolderLeaveBehind();
+        if (getParent() instanceof FolderIconParent) {
+            ((FolderIconParent) getParent()).clearFolderLeaveBehind(this);
         }
     }
 
     public void drawLeaveBehindIfExists() {
-        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) getLayoutParams();
-        // While the folder is open, the position of the icon cannot change.
-        lp.canReorder = false;
-        if (mInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
-            CellLayout cl = (CellLayout) getParent().getParent();
-            cl.setFolderLeaveBehindCell(lp.cellX, lp.cellY);
+        if (getParent() instanceof FolderIconParent) {
+            ((FolderIconParent) getParent()).drawFolderLeaveBehindForIcon(this);
         }
     }
 
@@ -828,4 +830,19 @@
                     MAX_NUM_ITEMS_IN_PREVIEW);
         }
     }
+
+    /**
+     * Interface that provides callbacks to a parent ViewGroup that hosts this FolderIcon.
+     */
+    public interface FolderIconParent {
+        /**
+         * Tells the FolderIconParent to draw a "leave-behind" when the Folder is open and leaving a
+         * gap where the FolderIcon would be when the Folder is closed.
+         */
+        void drawFolderLeaveBehindForIcon(FolderIcon child);
+        /**
+         * Tells the FolderIconParent to stop drawing the "leave-behind" as the Folder is closed.
+         */
+        void clearFolderLeaveBehind(FolderIcon child);
+    }
 }
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index a08dd30..0235dfa 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -36,7 +36,6 @@
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
@@ -193,7 +192,7 @@
         int pageNo = rank / mOrganizer.getMaxItemsPerPage();
 
         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams();
-        lp.setXY(mOrganizer.getPosForRank(rank));
+        lp.setCellXY(mOrganizer.getPosForRank(rank));
         getPageAt(pageNo).addViewToCellLayout(view, -1, item.getViewId(), lp, true);
     }
 
@@ -230,7 +229,7 @@
     }
 
     private CellLayout createAndAddNewPage() {
-        DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile();
+        DeviceProfile grid = mFolder.mActivityContext.getDeviceProfile();
         CellLayout page = mViewCache.getView(R.layout.folder_page, getContext(), this);
         page.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx);
         page.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false);
@@ -306,7 +305,7 @@
             if (v != null) {
                 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
                 ItemInfo info = (ItemInfo) v.getTag();
-                lp.setXY(mOrganizer.getPosForRank(rank));
+                lp.setCellXY(mOrganizer.getPosForRank(rank));
                 currentPage.addViewToCellLayout(v, -1, info.getViewId(), lp, true);
 
                 if (mOrganizer.isItemInPreview(rank) && v instanceof BubbleTextView) {
@@ -624,7 +623,7 @@
 
     @Override
     protected boolean canScroll(float absVScroll, float absHScroll) {
-        return AbstractFloatingView.getTopOpenViewWithType(mFolder.mLauncher,
+        return AbstractFloatingView.getTopOpenViewWithType(mFolder.mActivityContext,
                 TYPE_ALL & ~TYPE_FOLDER) == null;
     }
 
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
index 27b906b..767fffe 100644
--- a/src/com/android/launcher3/folder/PreviewBackground.java
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -74,6 +74,7 @@
     int previewSize;
     int basePreviewOffsetX;
     int basePreviewOffsetY;
+    int paddingY;
 
     private CellLayout mDrawingDelegate;
 
@@ -157,7 +158,7 @@
         previewSize = grid.folderIconSizePx;
 
         basePreviewOffsetX = (availableSpaceX - previewSize) / 2;
-        basePreviewOffsetY = topPadding + grid.folderIconOffsetYPx;
+        basePreviewOffsetY = paddingY + topPadding + grid.folderIconOffsetYPx;
 
         // Stroke width is 1dp
         mStrokeWidth = context.getResources().getDisplayMetrics().density;
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 3a9986e..5e9b179 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -58,7 +58,6 @@
 import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
@@ -91,6 +90,7 @@
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.util.ArrayList;
diff --git a/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java b/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
index d347e8f..b6d25c4 100644
--- a/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PlaceHolderIconDrawable.java
@@ -19,10 +19,19 @@
 
 import static com.android.launcher3.graphics.IconShape.getShapePath;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import androidx.core.graphics.ColorUtils;
 
 import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.R;
@@ -53,4 +62,27 @@
         canvas.drawPath(mProgressPath, mPaint);
         canvas.restoreToCount(saveCount);
     }
+
+    /** Updates this placeholder to {@code newIcon} with animation. */
+    public void animateIconUpdate(Drawable newIcon) {
+        int placeholderColor = mPaint.getColor();
+        int originalAlpha = Color.alpha(placeholderColor);
+
+        ValueAnimator iconUpdateAnimation = ValueAnimator.ofInt(originalAlpha, 0);
+        iconUpdateAnimation.setDuration(375);
+        iconUpdateAnimation.addUpdateListener(valueAnimator -> {
+            int newAlpha = (int) valueAnimator.getAnimatedValue();
+            int newColor = ColorUtils.setAlphaComponent(placeholderColor, newAlpha);
+
+            newIcon.setColorFilter(new PorterDuffColorFilter(newColor, PorterDuff.Mode.SRC_ATOP));
+        });
+        iconUpdateAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                newIcon.setColorFilter(null);
+            }
+        });
+        iconUpdateAnimation.start();
+    }
+
 }
diff --git a/src/com/android/launcher3/graphics/PreloadIconDrawable.java b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
index 304d496..ce824df 100644
--- a/src/com/android/launcher3/graphics/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/graphics/PreloadIconDrawable.java
@@ -24,6 +24,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import android.content.Context;
+import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Matrix;
@@ -34,10 +35,12 @@
 import android.util.Pair;
 import android.util.Property;
 import android.util.SparseArray;
+import android.view.ContextThemeWrapper;
 
 import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.util.Themes;
 
 import java.lang.ref.WeakReference;
 
@@ -77,6 +80,9 @@
     private static final SparseArray<WeakReference<Pair<Path, Bitmap>>> sShadowCache =
             new SparseArray<>();
 
+    private static final int PRELOAD_ACCENT_COLOR_INDEX = 0;
+    private static final int PRELOAD_BACKGROUND_COLOR_INDEX = 1;
+
     private final Matrix mTmpMatrix = new Matrix();
     private final PathMeasure mPathMeasure = new PathMeasure();
 
@@ -91,6 +97,9 @@
 
     private Bitmap mShadowBitmap;
     private final int mIndicatorColor;
+    private final int mSystemAccentColor;
+    private final int mSystemBackgroundColor;
+    private final boolean mIsDarkMode;
 
     private int mTrackAlpha;
     private float mTrackLength;
@@ -104,11 +113,23 @@
 
     private ObjectAnimator mCurrentAnim;
 
+    private boolean mIsStartable;
+
     public PreloadIconDrawable(ItemInfoWithIcon info, Context context) {
-        this(info, IconPalette.getPreloadProgressColor(context, info.bitmap.color));
+        this(
+                info,
+                IconPalette.getPreloadProgressColor(context, info.bitmap.color),
+                getPreloadColors(context),
+            (context.getResources().getConfiguration().uiMode
+                    & Configuration.UI_MODE_NIGHT_MASK
+                    & Configuration.UI_MODE_NIGHT_YES) != 0) /* isDarkMode */;
     }
 
-    public PreloadIconDrawable(ItemInfoWithIcon info, int indicatorColor) {
+    public PreloadIconDrawable(
+            ItemInfoWithIcon info,
+            int indicatorColor,
+            int[] preloadColors,
+            boolean isDarkMode) {
         super(info.bitmap);
         mItem = info;
         mShapePath = getShapePath();
@@ -120,9 +141,12 @@
         mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
         mIndicatorColor = indicatorColor;
 
-        setInternalProgress(0);
+        mSystemAccentColor = preloadColors[PRELOAD_ACCENT_COLOR_INDEX];
+        mSystemBackgroundColor = preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX];
+        mIsDarkMode = isDarkMode;
 
-        setIsDisabled(!info.isAppStartable());
+        setInternalProgress(info.getProgressLevel());
+        setIsStartable(info.isAppStartable());
     }
 
     @Override
@@ -148,7 +172,7 @@
     }
 
     private Bitmap getShadowBitmap(int width, int height, float shadowRadius) {
-        int key = (width << 16) | height;
+        int key = ((width << 16) | height) * (mIsDarkMode ? -1 : 1);
         WeakReference<Pair<Path, Bitmap>> shadowRef = sShadowCache.get(key);
         Pair<Path, Bitmap> cache = shadowRef != null ? shadowRef.get() : null;
         Bitmap shadow = cache != null && cache.first.equals(mShapePath) ? cache.second : null;
@@ -157,8 +181,9 @@
         }
         shadow = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
         Canvas c = new Canvas(shadow);
-        mProgressPaint.setShadowLayer(shadowRadius, 0, 0, COLOR_SHADOW);
-        mProgressPaint.setColor(COLOR_TRACK);
+        mProgressPaint.setShadowLayer(shadowRadius, 0, 0, mIsStartable
+                ? COLOR_SHADOW : mSystemAccentColor);
+        mProgressPaint.setColor(mIsStartable ? COLOR_TRACK : mSystemBackgroundColor);
         mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
         c.drawPath(mScaledTrackPath, mProgressPaint);
         mProgressPaint.clearShadowLayer();
@@ -176,7 +201,7 @@
         }
 
         // Draw track.
-        mProgressPaint.setColor(mIndicatorColor);
+        mProgressPaint.setColor(mIsStartable ? mIndicatorColor : mSystemAccentColor);
         mProgressPaint.setAlpha(mTrackAlpha);
         if (mShadowBitmap != null) {
             canvas.drawBitmap(mShadowBitmap, bounds.left, bounds.top, mProgressPaint);
@@ -215,6 +240,14 @@
         return !mRanFinishAnimation;
     }
 
+    /** Sets whether this icon should display the startable app UI. */
+    public void setIsStartable(boolean isStartable) {
+        if (mIsStartable != isStartable) {
+            mIsStartable = isStartable;
+            setIsDisabled(!isStartable);
+        }
+    }
+
     private void updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish) {
         if (mCurrentAnim != null) {
             mCurrentAnim.cancel();
@@ -295,6 +328,18 @@
         invalidateSelf();
     }
 
+    private static int[] getPreloadColors(Context context) {
+        Context dayNightThemeContext = new ContextThemeWrapper(
+                context, android.R.style.Theme_DeviceDefault_DayNight);
+        int[] preloadColors = new int[2];
+
+        preloadColors[PRELOAD_ACCENT_COLOR_INDEX] = Themes.getColorAccent(dayNightThemeContext);
+        preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX] = Themes.getColorBackgroundFloating(
+                dayNightThemeContext);
+
+        return preloadColors;
+    }
+
     /**
      * Returns a FastBitmapDrawable with the icon.
      */
@@ -305,13 +350,21 @@
     @Override
     public ConstantState getConstantState() {
         return new PreloadIconConstantState(
-                mBitmap, mIconColor, !mItem.isAppStartable(), mItem, mIndicatorColor);
+                mBitmap,
+                mIconColor,
+                !mItem.isAppStartable(),
+                mItem,
+                mIndicatorColor,
+                new int[] {mSystemAccentColor, mSystemBackgroundColor},
+                mIsDarkMode);
     }
 
     protected static class PreloadIconConstantState extends FastBitmapConstantState {
 
         protected final ItemInfoWithIcon mInfo;
         protected final int mIndicatorColor;
+        protected final int[] mPreloadColors;
+        protected final boolean mIsDarkMode;
         protected final int mLevel;
 
         public PreloadIconConstantState(
@@ -319,19 +372,24 @@
                 int iconColor,
                 boolean isDisabled,
                 ItemInfoWithIcon info,
-                int indicatorcolor) {
+                int indicatorColor,
+                int[] preloadColors,
+                boolean isDarkMode) {
             super(bitmap, iconColor, isDisabled);
             mInfo = info;
-            mIndicatorColor = indicatorcolor;
+            mIndicatorColor = indicatorColor;
+            mPreloadColors = preloadColors;
+            mIsDarkMode = isDarkMode;
             mLevel = info.getProgressLevel();
         }
 
         @Override
         public PreloadIconDrawable newDrawable() {
-            PreloadIconDrawable drawable = new PreloadIconDrawable(mInfo, mIndicatorColor);
-            drawable.setLevel(mLevel);
-            drawable.setIsDisabled(mIsDisabled);
-            return drawable;
+            return new PreloadIconDrawable(
+                    mInfo,
+                    mIndicatorColor,
+                    mPreloadColors,
+                    mIsDarkMode);
         }
 
         @Override
diff --git a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java b/src/com/android/launcher3/graphics/SysUiScrim.java
similarity index 83%
rename from src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
rename to src/com/android/launcher3/graphics/SysUiScrim.java
index 7b7ab5e..d9c648b 100644
--- a/src/com/android/launcher3/graphics/WorkspaceAndHotseatScrim.java
+++ b/src/com/android/launcher3/graphics/SysUiScrim.java
@@ -34,7 +34,6 @@
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.graphics.Region;
 import android.graphics.Shader;
 import android.graphics.drawable.Drawable;
 import android.util.DisplayMetrics;
@@ -44,41 +43,39 @@
 
 import androidx.core.graphics.ColorUtils;
 
-import com.android.launcher3.CellLayout;
 import com.android.launcher3.R;
 import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.Workspace;
 import com.android.launcher3.uioverrides.WallpaperColorInfo;
 import com.android.launcher3.util.Themes;
 
 /**
  * View scrim which draws behind hotseat and workspace
  */
-public class WorkspaceAndHotseatScrim extends Scrim {
+public class SysUiScrim extends Scrim {
 
-    public static final FloatProperty<WorkspaceAndHotseatScrim> SYSUI_PROGRESS =
-            new FloatProperty<WorkspaceAndHotseatScrim>("sysUiProgress") {
+    public static final FloatProperty<SysUiScrim> SYSUI_PROGRESS =
+            new FloatProperty<SysUiScrim>("sysUiProgress") {
                 @Override
-                public Float get(WorkspaceAndHotseatScrim scrim) {
+                public Float get(SysUiScrim scrim) {
                     return scrim.mSysUiProgress;
                 }
 
                 @Override
-                public void setValue(WorkspaceAndHotseatScrim scrim, float value) {
+                public void setValue(SysUiScrim scrim, float value) {
                     scrim.setSysUiProgress(value);
                 }
             };
 
-    private static final FloatProperty<WorkspaceAndHotseatScrim> SYSUI_ANIM_MULTIPLIER =
-            new FloatProperty<WorkspaceAndHotseatScrim>("sysUiAnimMultiplier") {
+    private static final FloatProperty<SysUiScrim> SYSUI_ANIM_MULTIPLIER =
+            new FloatProperty<SysUiScrim>("sysUiAnimMultiplier") {
                 @Override
-                public Float get(WorkspaceAndHotseatScrim scrim) {
+                public Float get(SysUiScrim scrim) {
                     return scrim.mSysUiAnimMultiplier;
                 }
 
                 @Override
-                public void setValue(WorkspaceAndHotseatScrim scrim, float value) {
+                public void setValue(SysUiScrim scrim, float value) {
                     scrim.mSysUiAnimMultiplier = value;
                     scrim.reapplySysUiAlpha();
                 }
@@ -108,10 +105,6 @@
     private static final int ALPHA_MASK_BITMAP_DP = 200;
     private static final int ALPHA_MASK_WIDTH_DP = 2;
 
-    private final Rect mHighlightRect = new Rect();
-
-    private Workspace mWorkspace;
-
     private boolean mDrawTopScrim, mDrawBottomScrim;
 
     private final RectF mFinalMaskRect = new RectF();
@@ -127,9 +120,8 @@
     private boolean mAnimateScrimOnNextDraw = false;
     private float mSysUiAnimMultiplier = 1;
 
-    public WorkspaceAndHotseatScrim(View view) {
+    public SysUiScrim(View view) {
         super(view);
-
         mMaskHeight = ResourceUtils.pxFromDp(ALPHA_MASK_BITMAP_DP,
                 view.getResources().getDisplayMetrics());
         mTopScrim = Themes.getAttrDrawable(view.getContext(), R.attr.workspaceStatusBarScrim);
@@ -139,28 +131,10 @@
         onExtractedColorsChanged(mWallpaperColorInfo);
     }
 
-    public void setWorkspace(Workspace workspace)  {
-        mWorkspace = workspace;
-    }
-
+    /**
+     * Draw the top and bottom scrims
+     */
     public void draw(Canvas canvas) {
-        // Draw the background below children.
-        if (mScrimAlpha > 0) {
-            // Update the scroll position first to ensure scrim cutout is in the right place.
-            mWorkspace.computeScrollWithoutInvalidation();
-            CellLayout currCellLayout = mWorkspace.getCurrentDragOverlappingLayout();
-            canvas.save();
-            if (currCellLayout != null && currCellLayout != mLauncher.getHotseat()) {
-                // Cut a hole in the darkening scrim on the page that should be highlighted, if any.
-                mLauncher.getDragLayer()
-                        .getDescendantRectRelativeToSelf(currCellLayout, mHighlightRect);
-                canvas.clipRect(mHighlightRect, Region.Op.DIFFERENCE);
-            }
-
-            super.draw(canvas);
-            canvas.restore();
-        }
-
         if (!mHideSysUiScrim) {
             if (mSysUiProgress <= 0) {
                 mAnimateScrimOnNextDraw = false;
@@ -247,6 +221,11 @@
         super.onExtractedColorsChanged(wallpaperColorInfo);
     }
 
+    /**
+     * Set the width and height of the view being scrimmed
+     * @param w
+     * @param h
+     */
     public void setSize(int w, int h) {
         if (mTopScrim != null) {
             mTopScrim.setBounds(0, 0, w, h);
diff --git a/src/com/android/launcher3/graphics/WorkspaceDragScrim.java b/src/com/android/launcher3/graphics/WorkspaceDragScrim.java
new file mode 100644
index 0000000..d8dc563
--- /dev/null
+++ b/src/com/android/launcher3/graphics/WorkspaceDragScrim.java
@@ -0,0 +1,74 @@
+/*
+ * 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.graphics;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.view.View;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.Workspace;
+
+/**
+ * Scrim drawn during SpringLoaded State (ie. Drag and Drop). Darkens the workspace except for
+ * the focused CellLayout.
+ */
+public class WorkspaceDragScrim extends Scrim {
+
+    private final Rect mHighlightRect = new Rect();
+
+    private Workspace mWorkspace;
+
+    public WorkspaceDragScrim(View view) {
+        super(view);
+        onExtractedColorsChanged(mWallpaperColorInfo);
+    }
+
+    /**
+     * Set the workspace that this scrim is acting on
+     * @param workspace
+     */
+    public void setWorkspace(Workspace workspace)  {
+        mWorkspace = workspace;
+        mWorkspace.setWorkspaceDragScrim(this);
+    }
+
+    /**
+     * Cut out the focused paged of the Workspace and then draw the scrim
+     * @param canvas
+     */
+    public void draw(Canvas canvas) {
+        // Draw the background below children.
+        if (mScrimAlpha > 0) {
+            // Update the scroll position first to ensure scrim cutout is in the right place.
+            mWorkspace.computeScrollWithoutInvalidation();
+            CellLayout currCellLayout = mWorkspace.getCurrentDragOverlappingLayout();
+            canvas.save();
+            if (currCellLayout != null && currCellLayout != mLauncher.getHotseat()) {
+                // Cut a hole in the darkening scrim on the page that should be highlighted, if any.
+                mLauncher.getDragLayer()
+                        .getDescendantRectRelativeToSelf(currCellLayout, mHighlightRect);
+                canvas.clipRect(mHighlightRect, Region.Op.DIFFERENCE);
+            }
+
+            super.draw(canvas);
+            canvas.restore();
+        }
+    }
+
+}
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index 8013557..61f2c2a 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -31,7 +31,6 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ShortcutInfo;
 import android.graphics.drawable.Drawable;
-import android.os.Handler;
 import android.os.Process;
 import android.os.UserHandle;
 import android.util.Log;
@@ -134,7 +133,7 @@
      * Fetches high-res icon for the provided ItemInfo and updates the caller when done.
      * @return a request ID that can be used to cancel the request.
      */
-    public IconLoadRequest updateIconInBackground(final ItemInfoUpdateReceiver caller,
+    public HandlerRunnable updateIconInBackground(final ItemInfoUpdateReceiver caller,
             final ItemInfoWithIcon info) {
         Preconditions.assertUIThread();
         if (mPendingIconRequestCount <= 0) {
@@ -142,20 +141,18 @@
         }
         mPendingIconRequestCount ++;
 
-        IconLoadRequest request = new IconLoadRequest(mWorkerHandler, this::onIconRequestEnd) {
-            @Override
-            public void run() {
-                if (info instanceof AppInfo || info instanceof WorkspaceItemInfo) {
-                    getTitleAndIcon(info, false);
-                } else if (info instanceof PackageItemInfo) {
-                    getTitleAndIconForApp((PackageItemInfo) info, false);
-                }
-                MAIN_EXECUTOR.execute(() -> {
-                    caller.reapplyItemInfo(info);
-                    onEnd();
-                });
-            }
-        };
+        HandlerRunnable<ItemInfoWithIcon> request = new HandlerRunnable<>(mWorkerHandler,
+                () -> {
+                    if (info instanceof AppInfo || info instanceof WorkspaceItemInfo) {
+                        getTitleAndIcon(info, false);
+                    } else if (info instanceof PackageItemInfo) {
+                        getTitleAndIconForApp((PackageItemInfo) info, false);
+                    }
+                    return info;
+                },
+                MAIN_EXECUTOR,
+                caller::reapplyItemInfo,
+                this::onIconRequestEnd);
         Utilities.postAsyncCallback(mWorkerHandler, request);
         return request;
     }
@@ -336,12 +333,6 @@
         return super.getEntryFromDB(cacheKey, entry, lowRes);
     }
 
-    public static abstract class IconLoadRequest extends HandlerRunnable {
-        IconLoadRequest(Handler handler, Runnable endRunnable) {
-            super(handler, endRunnable);
-        }
-    }
-
     /**
      * Interface for receiving itemInfo with high-res icon.
      */
diff --git a/src/com/android/launcher3/keyboard/CustomActionsPopup.java b/src/com/android/launcher3/keyboard/CustomActionsPopup.java
deleted file mode 100644
index 800598e..0000000
--- a/src/com/android/launcher3/keyboard/CustomActionsPopup.java
+++ /dev/null
@@ -1,93 +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.keyboard;
-
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
-import android.widget.PopupMenu;
-import android.widget.PopupMenu.OnMenuItemClickListener;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.popup.PopupContainerWithArrow;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Handles showing a popup menu with available custom actions for a launcher icon.
- * This allows exposing various custom actions using keyboard shortcuts.
- */
-public class CustomActionsPopup implements OnMenuItemClickListener {
-
-    private final Launcher mLauncher;
-    private final LauncherAccessibilityDelegate mDelegate;
-    private final View mIcon;
-
-    public CustomActionsPopup(Launcher launcher, View icon) {
-        mLauncher = launcher;
-        mIcon = icon;
-        PopupContainerWithArrow container = PopupContainerWithArrow.getOpen(launcher);
-        if (container != null) {
-            mDelegate = container.getAccessibilityDelegate();
-        } else {
-            mDelegate = launcher.getAccessibilityDelegate();
-        }
-    }
-
-    private List<AccessibilityAction> getActionList() {
-        if (mIcon == null || !(mIcon.getTag() instanceof ItemInfo)) {
-            return Collections.EMPTY_LIST;
-        }
-
-        AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
-        mDelegate.addSupportedActions(mIcon, info, true);
-        List<AccessibilityAction> result = new ArrayList<>(info.getActionList());
-        info.recycle();
-        return result;
-    }
-
-    public boolean canShow() {
-        return !getActionList().isEmpty();
-    }
-
-    public boolean show() {
-        List<AccessibilityAction> actions = getActionList();
-        if (actions.isEmpty()) {
-            return false;
-        }
-
-        PopupMenu popup = new PopupMenu(mLauncher, mIcon);
-        popup.setOnMenuItemClickListener(this);
-        Menu menu = popup.getMenu();
-        for (AccessibilityAction action : actions) {
-            menu.add(Menu.NONE, action.getId(), Menu.NONE, action.getLabel());
-        }
-        popup.show();
-        return true;
-    }
-
-    @Override
-    public boolean onMenuItemClick(MenuItem menuItem) {
-        return mDelegate.performAction(mIcon, (ItemInfo) mIcon.getTag(), menuItem.getItemId());
-    }
-}
diff --git a/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java b/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java
index ae7ad10..83003ff 100644
--- a/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java
+++ b/src/com/android/launcher3/keyboard/FocusIndicatorHelper.java
@@ -16,233 +16,30 @@
 
 package com.android.launcher3.keyboard;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.animation.RectEvaluator;
-import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
 import android.graphics.Rect;
-import android.util.Property;
 import android.view.View;
 import android.view.View.OnFocusChangeListener;
 
 import com.android.launcher3.R;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.util.Themes;
 
 /**
  * A helper class to draw background of a focused view.
  */
-public abstract class FocusIndicatorHelper implements
-        OnFocusChangeListener, AnimatorUpdateListener {
-
-    private static final float MIN_VISIBLE_ALPHA = 0.2f;
-    private static final long ANIM_DURATION = 150;
-
-    public static final Property<FocusIndicatorHelper, Float> ALPHA =
-            new Property<FocusIndicatorHelper, Float>(Float.TYPE, "alpha") {
-                @Override
-                public void set(FocusIndicatorHelper object, Float value) {
-                    object.setAlpha(value);
-                }
-
-                @Override
-                public Float get(FocusIndicatorHelper object) {
-                    return object.mAlpha;
-                }
-            };
-
-    public static final Property<FocusIndicatorHelper, Float> SHIFT =
-            new Property<FocusIndicatorHelper, Float>(
-                    Float.TYPE, "shift") {
-
-                @Override
-                public void set(FocusIndicatorHelper object, Float value) {
-                    object.mShift = value;
-                }
-
-                @Override
-                public Float get(FocusIndicatorHelper object) {
-                    return object.mShift;
-                }
-            };
-
-    private static final RectEvaluator RECT_EVALUATOR = new RectEvaluator(new Rect());
-    private static final Rect sTempRect1 = new Rect();
-    private static final Rect sTempRect2 = new Rect();
-
-    private final View mContainer;
-    private final Paint mPaint;
-    private final int mMaxAlpha;
-
-    private final Rect mDirtyRect = new Rect();
-    private boolean mIsDirty = false;
-
-    private View mLastFocusedView;
-
-    private View mCurrentView;
-    private View mTargetView;
-    /**
-     * The fraction indicating the position of the focusRect between {@link #mCurrentView}
-     * & {@link #mTargetView}
-     */
-    private float mShift;
-
-    private ObjectAnimator mCurrentAnimation;
-    private float mAlpha;
-    private float mRadius;
+public abstract class FocusIndicatorHelper extends ItemFocusIndicatorHelper<View>
+        implements OnFocusChangeListener {
 
     public FocusIndicatorHelper(View container) {
-        mContainer = container;
-
-        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        int color = container.getResources().getColor(R.color.focused_background);
-        mMaxAlpha = Color.alpha(color);
-        mPaint.setColor(0xFF000000 | color);
-
-        setAlpha(0);
-        mShift = 0;
-        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
-            mRadius = Themes.getDialogCornerRadius(container.getContext());
-        }
-    }
-
-    protected void setAlpha(float alpha) {
-        mAlpha = alpha;
-        mPaint.setAlpha((int) (mAlpha * mMaxAlpha));
-    }
-
-    @Override
-    public void onAnimationUpdate(ValueAnimator animation) {
-        invalidateDirty();
-    }
-
-    protected void invalidateDirty() {
-        if (mIsDirty) {
-            mContainer.invalidate(mDirtyRect);
-            mIsDirty = false;
-        }
-
-        Rect newRect = getDrawRect();
-        if (newRect != null) {
-            mContainer.invalidate(newRect);
-        }
-    }
-
-    public void draw(Canvas c) {
-        if (mAlpha <= 0) return;
-
-        Rect newRect = getDrawRect();
-        if (newRect != null) {
-            mDirtyRect.set(newRect);
-            c.drawRoundRect((float) mDirtyRect.left, (float) mDirtyRect.top,
-                    (float) mDirtyRect.right, (float) mDirtyRect.bottom,
-                    mRadius, mRadius, mPaint);
-            mIsDirty = true;
-        }
-    }
-
-    private Rect getDrawRect() {
-        if (mCurrentView != null && mCurrentView.isAttachedToWindow()) {
-            viewToRect(mCurrentView, sTempRect1);
-
-            if (mShift > 0 && mTargetView != null) {
-                viewToRect(mTargetView, sTempRect2);
-                return RECT_EVALUATOR.evaluate(mShift, sTempRect1, sTempRect2);
-            } else {
-                return sTempRect1;
-            }
-        }
-        return null;
+        super(container, container.getResources().getColor(R.color.focused_background));
     }
 
     @Override
     public void onFocusChange(View v, boolean hasFocus) {
-        if (hasFocus) {
-            endCurrentAnimation();
-
-            if (mAlpha > MIN_VISIBLE_ALPHA) {
-                mTargetView = v;
-
-                mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
-                        PropertyValuesHolder.ofFloat(ALPHA, 1),
-                        PropertyValuesHolder.ofFloat(SHIFT, 1));
-                mCurrentAnimation.addListener(new ViewSetListener(v, true));
-            } else {
-                setCurrentView(v);
-
-                mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
-                        PropertyValuesHolder.ofFloat(ALPHA, 1));
-            }
-
-            mLastFocusedView = v;
-        } else {
-            if (mLastFocusedView == v) {
-                mLastFocusedView = null;
-                endCurrentAnimation();
-                mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
-                        PropertyValuesHolder.ofFloat(ALPHA, 0));
-                mCurrentAnimation.addListener(new ViewSetListener(null, false));
-            }
-        }
-
-        // invalidate once
-        invalidateDirty();
-
-        mLastFocusedView = hasFocus ? v : null;
-        if (mCurrentAnimation != null) {
-            mCurrentAnimation.addUpdateListener(this);
-            mCurrentAnimation.setDuration(ANIM_DURATION).start();
-        }
+        changeFocus(v, hasFocus);
     }
 
-    protected void endCurrentAnimation() {
-        if (mCurrentAnimation != null) {
-            mCurrentAnimation.cancel();
-            mCurrentAnimation = null;
-        }
-    }
-
-    protected void setCurrentView(View v) {
-        mCurrentView = v;
-        mShift = 0;
-        mTargetView = null;
-    }
-
-    /**
-     * Gets the position of {@param v} relative to {@link #mContainer}.
-     */
-    public abstract void viewToRect(View v, Rect outRect);
-
-    private class ViewSetListener extends AnimatorListenerAdapter {
-        private final View mViewToSet;
-        private final boolean mCallOnCancel;
-        private boolean mCalled = false;
-
-        public ViewSetListener(View v, boolean callOnCancel) {
-            mViewToSet = v;
-            mCallOnCancel = callOnCancel;
-        }
-
-        @Override
-        public void onAnimationCancel(Animator animation) {
-            if (!mCallOnCancel) {
-                mCalled = true;
-            }
-        }
-
-        @Override
-        public void onAnimationEnd(Animator animation) {
-            if (!mCalled) {
-                setCurrentView(mViewToSet);
-                mCalled = true;
-            }
-        }
+    @Override
+    protected boolean shouldDraw(View item) {
+        return item.isAttachedToWindow();
     }
 
     /**
diff --git a/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java
new file mode 100644
index 0000000..57fab2d
--- /dev/null
+++ b/src/com/android/launcher3/keyboard/ItemFocusIndicatorHelper.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2021 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.keyboard;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.RectEvaluator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.FloatProperty;
+import android.view.View;
+
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.Themes;
+
+/**
+ * A helper class to draw background of a focused item.
+ * @param <T> Item type
+ */
+public abstract class ItemFocusIndicatorHelper<T> implements AnimatorUpdateListener {
+
+    private static final float MIN_VISIBLE_ALPHA = 0.2f;
+    private static final long ANIM_DURATION = 150;
+
+    public static final FloatProperty<ItemFocusIndicatorHelper> ALPHA =
+            new FloatProperty<ItemFocusIndicatorHelper>("alpha") {
+
+                @Override
+                public void setValue(ItemFocusIndicatorHelper object, float value) {
+                    object.setAlpha(value);
+                }
+
+                @Override
+                public Float get(ItemFocusIndicatorHelper object) {
+                    return object.mAlpha;
+                }
+            };
+
+    public static final FloatProperty<ItemFocusIndicatorHelper> SHIFT =
+            new FloatProperty<ItemFocusIndicatorHelper>("shift") {
+
+                @Override
+                public void setValue(ItemFocusIndicatorHelper object, float value) {
+                    object.mShift = value;
+                }
+
+                @Override
+                public Float get(ItemFocusIndicatorHelper object) {
+                    return object.mShift;
+                }
+            };
+
+    private static final RectEvaluator RECT_EVALUATOR = new RectEvaluator(new Rect());
+    private static final Rect sTempRect1 = new Rect();
+    private static final Rect sTempRect2 = new Rect();
+
+    private final View mContainer;
+    protected final Paint mPaint;
+    private final int mMaxAlpha;
+
+    private final Rect mDirtyRect = new Rect();
+    private boolean mIsDirty = false;
+
+    private T mLastFocusedItem;
+
+    private T mCurrentItem;
+    private T mTargetItem;
+    /**
+     * The fraction indicating the position of the focusRect between {@link #mCurrentItem}
+     * & {@link #mTargetItem}
+     */
+    private float mShift;
+
+    private ObjectAnimator mCurrentAnimation;
+    private float mAlpha;
+    private float mRadius;
+
+    public ItemFocusIndicatorHelper(View container, int color) {
+        mContainer = container;
+
+        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mMaxAlpha = Color.alpha(color);
+        mPaint.setColor(0xFF000000 | color);
+
+        setAlpha(0);
+        mShift = 0;
+        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+            mRadius = Themes.getDialogCornerRadius(container.getContext());
+        }
+    }
+
+    protected void setAlpha(float alpha) {
+        mAlpha = alpha;
+        mPaint.setAlpha((int) (mAlpha * mMaxAlpha));
+    }
+
+    @Override
+    public void onAnimationUpdate(ValueAnimator animation) {
+        invalidateDirty();
+    }
+
+    protected void invalidateDirty() {
+        if (mIsDirty) {
+            mContainer.invalidate(mDirtyRect);
+            mIsDirty = false;
+        }
+
+        Rect newRect = getDrawRect();
+        if (newRect != null) {
+            mContainer.invalidate(newRect);
+        }
+    }
+
+    /**
+     * Draws the indicator on the canvas
+     */
+    public void draw(Canvas c) {
+        if (mAlpha <= 0) return;
+
+        Rect newRect = getDrawRect();
+        if (newRect != null) {
+            mDirtyRect.set(newRect);
+            c.drawRoundRect((float) mDirtyRect.left, (float) mDirtyRect.top,
+                    (float) mDirtyRect.right, (float) mDirtyRect.bottom,
+                    mRadius, mRadius, mPaint);
+            mIsDirty = true;
+        }
+    }
+
+    private Rect getDrawRect() {
+        if (mCurrentItem != null && shouldDraw(mCurrentItem)) {
+            viewToRect(mCurrentItem, sTempRect1);
+
+            if (mShift > 0 && mTargetItem != null) {
+                viewToRect(mTargetItem, sTempRect2);
+                return RECT_EVALUATOR.evaluate(mShift, sTempRect1, sTempRect2);
+            } else {
+                return sTempRect1;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns true if the provided item is valid
+     */
+    protected boolean shouldDraw(T item) {
+        return true;
+    }
+
+    protected void changeFocus(T item, boolean hasFocus) {
+        if (hasFocus) {
+            endCurrentAnimation();
+
+            if (mAlpha > MIN_VISIBLE_ALPHA) {
+                mTargetItem = item;
+
+                mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
+                        PropertyValuesHolder.ofFloat(ALPHA, 1),
+                        PropertyValuesHolder.ofFloat(SHIFT, 1));
+                mCurrentAnimation.addListener(new ViewSetListener(item, true));
+            } else {
+                setCurrentItem(item);
+
+                mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
+                        PropertyValuesHolder.ofFloat(ALPHA, 1));
+            }
+
+            mLastFocusedItem = item;
+        } else {
+            if (mLastFocusedItem == item) {
+                mLastFocusedItem = null;
+                endCurrentAnimation();
+                mCurrentAnimation = ObjectAnimator.ofPropertyValuesHolder(this,
+                        PropertyValuesHolder.ofFloat(ALPHA, 0));
+                mCurrentAnimation.addListener(new ViewSetListener(null, false));
+            }
+        }
+
+        // invalidate once
+        invalidateDirty();
+
+        mLastFocusedItem = hasFocus ? item : null;
+        if (mCurrentAnimation != null) {
+            mCurrentAnimation.addUpdateListener(this);
+            mCurrentAnimation.setDuration(ANIM_DURATION).start();
+        }
+    }
+
+    protected void endCurrentAnimation() {
+        if (mCurrentAnimation != null) {
+            mCurrentAnimation.cancel();
+            mCurrentAnimation = null;
+        }
+    }
+
+    protected void setCurrentItem(T item) {
+        mCurrentItem = item;
+        mShift = 0;
+        mTargetItem = null;
+    }
+
+    /**
+     * Gets the position of the item relative to {@link #mContainer}.
+     */
+    public abstract void viewToRect(T item, Rect outRect);
+
+    private class ViewSetListener extends AnimatorListenerAdapter {
+        private final T mItemToSet;
+        private final boolean mCallOnCancel;
+        private boolean mCalled = false;
+
+        ViewSetListener(T item, boolean callOnCancel) {
+            mItemToSet = item;
+            mCallOnCancel = callOnCancel;
+        }
+
+        @Override
+        public void onAnimationCancel(Animator animation) {
+            if (!mCallOnCancel) {
+                mCalled = true;
+            }
+        }
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            if (!mCalled) {
+                setCurrentItem(mItemToSet);
+                mCalled = true;
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java b/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java
new file mode 100644
index 0000000..a6c897f
--- /dev/null
+++ b/src/com/android/launcher3/keyboard/KeyboardDragAndDropView.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2021 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.keyboard;
+
+import static android.app.Activity.DEFAULT_KEYS_SEARCH_LOCAL;
+
+import static com.android.launcher3.LauncherState.SPRING_LOADED;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint.Style;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewParent;
+import android.widget.TextView;
+
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.PagedView;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.folder.Folder;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.touch.ItemLongClickListener;
+import com.android.launcher3.util.Themes;
+
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.function.ToIntBiFunction;
+import java.util.function.ToIntFunction;
+
+/**
+ * A floating view to allow keyboard navigation across virtual nodes
+ */
+public class KeyboardDragAndDropView extends AbstractFloatingView
+        implements Insettable, StateListener<LauncherState> {
+
+    private static final long MINOR_AXIS_WEIGHT = 13;
+
+    private final ArrayList<Integer> mIntList = new ArrayList<>();
+    private final ArrayList<DragAndDropAccessibilityDelegate> mDelegates = new ArrayList<>();
+    private final ArrayList<VirtualNodeInfo> mNodes = new ArrayList<>();
+
+    private final Rect mTempRect = new Rect();
+    private final Rect mTempRect2 = new Rect();
+    private final AccessibilityNodeInfoCompat mTempNodeInfo = AccessibilityNodeInfoCompat.obtain();
+
+    private final RectFocusIndicator mFocusIndicator;
+
+    private final Launcher mLauncher;
+    private VirtualNodeInfo mCurrentSelection;
+
+
+    public KeyboardDragAndDropView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public KeyboardDragAndDropView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mLauncher = Launcher.getLauncher(context);
+        mFocusIndicator = new RectFocusIndicator(this);
+        setWillNotDraw(false);
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        mLauncher.getDragLayer().removeView(this);
+        mLauncher.getStateManager().removeStateListener(this);
+        mLauncher.setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
+        mIsOpen = false;
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_DRAG_DROP_POPUP) != 0;
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        // Consume all touch
+        return true;
+    }
+
+    @Override
+    public void setInsets(Rect insets) {
+        setPadding(insets.left, insets.top, insets.right, insets.bottom);
+    }
+
+    @Override
+    public void onStateTransitionStart(LauncherState toState) {
+        if (toState != SPRING_LOADED) {
+            close(false);
+        }
+    }
+
+    @Override
+    public void onStateTransitionComplete(LauncherState finalState) {
+        if (mCurrentSelection != null) {
+            setCurrentSelection(mCurrentSelection);
+        }
+    }
+
+    private void setCurrentSelection(VirtualNodeInfo nodeInfo) {
+        mCurrentSelection = nodeInfo;
+        ((TextView) findViewById(R.id.label))
+                .setText(nodeInfo.populate(mTempNodeInfo).getContentDescription());
+
+        Rect bounds = new Rect();
+        mTempNodeInfo.getBoundsInParent(bounds);
+        View host = nodeInfo.delegate.getHost();
+        ViewParent parent = host.getParent();
+        if (parent instanceof PagedView) {
+            PagedView pv = (PagedView) parent;
+            int pageIndex = pv.indexOfChild(host);
+
+            pv.setCurrentPage(pageIndex);
+            bounds.offset(pv.getScrollX() - pv.getScrollForPage(pageIndex), 0);
+        }
+        float[] pos = new float[] {bounds.left, bounds.top, bounds.right, bounds.bottom};
+        Utilities.getDescendantCoordRelativeToAncestor(host, mLauncher.getDragLayer(), pos, true);
+
+        new RectF(pos[0], pos[1], pos[2], pos[3]).roundOut(bounds);
+        mFocusIndicator.changeFocus(bounds, true);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        mFocusIndicator.draw(canvas);
+    }
+
+    @Override
+    public boolean dispatchUnhandledMove(View focused, int direction) {
+        VirtualNodeInfo nodeInfo = getNextSelection(direction);
+        if (nodeInfo == null) {
+            return false;
+        }
+        setCurrentSelection(nodeInfo);
+        return true;
+    }
+
+    /**
+     * Focus finding logic:
+     * Collect all virtual nodes in reading order (used for forward and backwards).
+     * Then find the closest view by comparing the distances spatially. Since it is a move
+     * operation. consider all cell sizes to be approximately of the same size.
+     */
+    private VirtualNodeInfo getNextSelection(int direction) {
+        // Collect all virtual nodes
+        mDelegates.clear();
+        mNodes.clear();
+
+        Folder openFolder = Folder.getOpen(mLauncher);
+        PagedView pv = openFolder == null ? mLauncher.getWorkspace() : openFolder.getContent();
+        int count = pv.getPageCount();
+        for (int i = 0; i < count; i++) {
+            mDelegates.add(((CellLayout) pv.getChildAt(i)).getDragAndDropAccessibilityDelegate());
+        }
+        if (openFolder == null) {
+            mDelegates.add(pv.getNextPage() + 1,
+                    mLauncher.getHotseat().getDragAndDropAccessibilityDelegate());
+        }
+        mDelegates.forEach(delegate -> {
+            mIntList.clear();
+            delegate.getVisibleVirtualViews(mIntList);
+            mIntList.forEach(id -> mNodes.add(new VirtualNodeInfo(delegate, id)));
+        });
+
+        if (mNodes.isEmpty()) {
+            return null;
+        }
+        int index = mNodes.indexOf(mCurrentSelection);
+        if (mCurrentSelection == null || index < 0) {
+            return null;
+        }
+        int totalNodes = mNodes.size();
+
+        final ToIntBiFunction<Rect, Rect> majorAxis;
+        final ToIntFunction<Rect> minorAxis;
+
+        switch (direction) {
+            case View.FOCUS_RIGHT:
+                majorAxis = (source, dest) -> dest.left - source.left;
+                minorAxis = Rect::centerY;
+                break;
+            case View.FOCUS_LEFT:
+                majorAxis = (source, dest) -> source.left - dest.left;
+                minorAxis = Rect::centerY;
+                break;
+            case View.FOCUS_UP:
+                majorAxis = (source, dest) -> source.top - dest.top;
+                minorAxis = Rect::centerX;
+                break;
+            case View.FOCUS_DOWN:
+                majorAxis = (source, dest) -> dest.top - source.top;
+                minorAxis = Rect::centerX;
+                break;
+            case View.FOCUS_FORWARD:
+                return mNodes.get((index + 1) % totalNodes);
+            case View.FOCUS_BACKWARD:
+                return mNodes.get((index + totalNodes - 1) % totalNodes);
+            default:
+                // Unknown direction
+                return null;
+        }
+        mCurrentSelection.populate(mTempNodeInfo).getBoundsInScreen(mTempRect);
+
+        float minWeight = Float.MAX_VALUE;
+        VirtualNodeInfo match = null;
+        for (int i = 0; i < totalNodes; i++) {
+            VirtualNodeInfo node = mNodes.get(i);
+            node.populate(mTempNodeInfo).getBoundsInScreen(mTempRect2);
+
+            int majorAxisWeight = majorAxis.applyAsInt(mTempRect, mTempRect2);
+            if (majorAxisWeight <= 0) {
+                continue;
+            }
+            int minorAxisWeight = minorAxis.applyAsInt(mTempRect2)
+                    - minorAxis.applyAsInt(mTempRect);
+
+            float weight = majorAxisWeight * majorAxisWeight
+                    + minorAxisWeight * minorAxisWeight * MINOR_AXIS_WEIGHT;
+            if (weight < minWeight) {
+                minWeight = weight;
+                match = node;
+            }
+        }
+        return match;
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_ENTER && mCurrentSelection != null) {
+            mCurrentSelection.delegate.onPerformActionForVirtualView(
+                    mCurrentSelection.id, AccessibilityNodeInfoCompat.ACTION_CLICK, null);
+            return true;
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    /**
+     * Shows the keyboard drag popup for the provided view
+     */
+    public void showForIcon(View icon, ItemInfo item, DragOptions dragOptions) {
+        mIsOpen = true;
+        mLauncher.getDragLayer().addView(this);
+        mLauncher.getStateManager().addStateListener(this);
+
+        // Find current selection
+        CellLayout currentParent = (CellLayout) icon.getParent().getParent();
+        float[] iconPos = new float[] {currentParent.getCellWidth() / 2,
+                currentParent.getCellHeight() / 2};
+        Utilities.getDescendantCoordRelativeToAncestor(icon, currentParent, iconPos, false);
+
+        ItemLongClickListener.beginDrag(icon, mLauncher, item, dragOptions);
+
+        DragAndDropAccessibilityDelegate dndDelegate =
+                currentParent.getDragAndDropAccessibilityDelegate();
+        setCurrentSelection(new VirtualNodeInfo(
+                dndDelegate, dndDelegate.getVirtualViewAt(iconPos[0], iconPos[1])));
+
+        mLauncher.setDefaultKeyMode(Activity.DEFAULT_KEYS_DISABLE);
+        requestFocus();
+    }
+
+    private static class VirtualNodeInfo {
+        public final DragAndDropAccessibilityDelegate delegate;
+        public final int id;
+
+        VirtualNodeInfo(DragAndDropAccessibilityDelegate delegate, int id) {
+            this.id = id;
+            this.delegate = delegate;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (!(o instanceof VirtualNodeInfo)) {
+                return false;
+            }
+            VirtualNodeInfo that = (VirtualNodeInfo) o;
+            return id == that.id && delegate.equals(that.delegate);
+        }
+
+        public AccessibilityNodeInfoCompat populate(AccessibilityNodeInfoCompat nodeInfo) {
+            delegate.onPopulateNodeForVirtualView(id, nodeInfo);
+            return nodeInfo;
+        }
+
+        public void getBounds(AccessibilityNodeInfoCompat nodeInfo, Rect out) {
+            delegate.onPopulateNodeForVirtualView(id, nodeInfo);
+            nodeInfo.getBoundsInScreen(out);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(id, delegate);
+        }
+    }
+
+    private static class RectFocusIndicator extends ItemFocusIndicatorHelper<Rect> {
+
+        RectFocusIndicator(View container) {
+            super(container, Themes.getColorAccent(container.getContext()));
+            mPaint.setStrokeWidth(container.getResources()
+                    .getDimension(R.dimen.keyboard_drag_stroke_width));
+            mPaint.setStyle(Style.STROKE);
+        }
+
+        @Override
+        public void viewToRect(Rect item, Rect outRect) {
+            outRect.set(item);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/logging/FileLog.java b/src/com/android/launcher3/logging/FileLog.java
index 6bc1ecb..cdd0bda 100644
--- a/src/com/android/launcher3/logging/FileLog.java
+++ b/src/com/android/launcher3/logging/FileLog.java
@@ -10,7 +10,6 @@
 
 import androidx.annotation.VisibleForTesting;
 
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.IOUtils;
 
 import java.io.BufferedReader;
@@ -43,7 +42,7 @@
     private static Handler sHandler = null;
     private static File sLogsDirectory = null;
 
-    public static final int LOG_DAYS = FeatureFlags.ENABLE_HYBRID_HOTSEAT.get() ? 10 : 4;
+    public static final int LOG_DAYS = 4;
 
     public static void setDir(File logsDir) {
         if (ENABLED) {
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 2066cd3..c5ffff9 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -21,6 +21,10 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_GESTURE;
 
 import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+import androidx.slice.SliceItem;
 
 import com.android.launcher3.R;
 import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
@@ -47,6 +51,7 @@
     public static final int LAUNCHER_STATE_ALLAPPS = 4;
     public static final int LAUNCHER_STATE_UNCHANGED = 5;
 
+    private InstanceId mInstanceId;
     /**
      * Returns event enum based on the two state transition information when swipe
      * gesture happens(to be removed during UserEventDispatcher cleanup).
@@ -95,9 +100,13 @@
         @UiEvent(doc = "User dragged a launcher item")
         LAUNCHER_ITEM_DRAG_STARTED(383),
 
-        @UiEvent(doc = "A dragged launcher item is successfully dropped")
+        @UiEvent(doc = "A dragged launcher item is successfully dropped onto workspace, hotseat "
+                + "open folder etc")
         LAUNCHER_ITEM_DROP_COMPLETED(385),
 
+        @UiEvent(doc = "A dragged launcher item is successfully dropped onto a folder icon.")
+        LAUNCHER_ITEM_DROP_COMPLETED_ON_FOLDER_ICON(697),
+
         @UiEvent(doc = "A dragged launcher item is successfully dropped on another item "
                 + "resulting in a new folder creation")
         LAUNCHER_ITEM_DROP_FOLDER_CREATED(386),
@@ -344,6 +353,67 @@
 
         @UiEvent(doc = "Current grid size is changed to 2.")
         LAUNCHER_GRID_SIZE_2(665),
+
+        @UiEvent(doc = "Launcher entered into AllApps state.")
+        LAUNCHER_ALLAPPS_ENTRY(692),
+
+        @UiEvent(doc = "Launcher exited from AllApps state.")
+        LAUNCHER_ALLAPPS_EXIT(693),
+
+        @UiEvent(doc = "User closed the AllApps keyboard.")
+        LAUNCHER_ALLAPPS_KEYBOARD_CLOSED(694),
+
+        @UiEvent(doc = "User switched to AllApps Main/Personal tab by swiping left.")
+        LAUNCHER_ALLAPPS_SWIPE_TO_PERSONAL_TAB(695),
+
+        @UiEvent(doc = "User switched to AllApps Work tab by swiping right.")
+        LAUNCHER_ALLAPPS_SWIPE_TO_WORK_TAB(696),
+
+        @UiEvent(doc = "Default event when dedicated UI event is not available for the user action"
+                + " on slice .")
+        LAUNCHER_SLICE_DEFAULT_ACTION(700),
+
+        @UiEvent(doc = "User toggled-on a Slice item.")
+        LAUNCHER_SLICE_TOGGLE_ON(701),
+
+        @UiEvent(doc = "User toggled-off a Slice item.")
+        LAUNCHER_SLICE_TOGGLE_OFF(702),
+
+        @UiEvent(doc = "User acted on a Slice item with a button.")
+        LAUNCHER_SLICE_BUTTON_ACTION(703),
+
+        @UiEvent(doc = "User acted on a Slice item with a slider.")
+        LAUNCHER_SLICE_SLIDER_ACTION(704),
+
+        @UiEvent(doc = "User tapped on the entire row of a Slice.")
+        LAUNCHER_SLICE_CONTENT_ACTION(705),
+
+        @UiEvent(doc = "User tapped on the see more button of a Slice.")
+        LAUNCHER_SLICE_SEE_MORE_ACTION(706),
+
+        @UiEvent(doc = "User selected from a selection row of Slice.")
+        LAUNCHER_SLICE_SELECTION_ACTION(707),
+
+        @UiEvent(doc = "IME is used for selecting the focused item on the AllApps screen.")
+        LAUNCHER_ALLAPPS_FOCUSED_ITEM_SELECTED_WITH_IME(718),
+
+        @UiEvent(doc = "User long-pressed on an AllApps item.")
+        LAUNCHER_ALLAPPS_ITEM_LONG_PRESSED(719),
+
+        @UiEvent(doc = "Launcher entered into AllApps state with device search enabled.")
+        LAUNCHER_ALLAPPS_ENTRY_WITH_DEVICE_SEARCH(720),
+
+        @UiEvent(doc = "User switched to AllApps Main/Personal tab by tapping on it.")
+        LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB(721),
+
+        @UiEvent(doc = "User switched to AllApps Work tab by tapping on it.")
+        LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB(722),
+
+        @UiEvent(doc = "All apps vertical fling started.")
+        LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN(724),
+
+        @UiEvent(doc = "All apps vertical fling ended.")
+        LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END(725)
         ;
 
         // ADD MORE
@@ -451,26 +521,56 @@
         }
 
         /**
+         * Sets logging fields from provided {@link SliceItem}.
+         */
+        default StatsLogger withSliceItem(SliceItem sliceItem) {
+            return this;
+        }
+
+        /**
          * Builds the final message and logs it as {@link EventEnum}.
          */
         default void log(EventEnum event) {
         }
+
+        /**
+         * Builds the final message and logs it to two different atoms, one for
+         * event tracking and the other for jank tracking.
+         */
+        default void sendToInteractionJankMonitor(EventEnum event, View v) {
+        }
     }
 
     /**
      * Returns new logger object.
      */
     public StatsLogger logger() {
+        StatsLogger logger = createLogger();
+        if (mInstanceId != null) {
+            return logger.withInstanceId(mInstanceId);
+        }
+        return logger;
+    }
+
+    protected StatsLogger createLogger() {
         return new StatsLogger() {
         };
     }
 
     /**
+     * Sets InstanceId to every new {@link StatsLogger} object returned by {@link #logger()} when
+     * not-null.
+     */
+    public StatsLogManager withDefaultInstanceId(@Nullable InstanceId instanceId) {
+        this.mInstanceId = instanceId;
+        return this;
+    }
+
+    /**
      * Creates a new instance of {@link StatsLogManager} based on provided context.
      */
     public static StatsLogManager newInstance(Context context) {
-        StatsLogManager mgr = Overrides.getObject(StatsLogManager.class,
+        return Overrides.getObject(StatsLogManager.class,
                 context.getApplicationContext(), R.string.stats_log_manager_class);
-        return mgr;
     }
 }
diff --git a/src/com/android/launcher3/model/AllAppsList.java b/src/com/android/launcher3/model/AllAppsList.java
index d4fa278..92b5885 100644
--- a/src/com/android/launcher3/model/AllAppsList.java
+++ b/src/com/android/launcher3/model/AllAppsList.java
@@ -22,7 +22,6 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
 import android.os.LocaleList;
@@ -137,7 +136,7 @@
         if (findAppInfo(info.componentName, info.user) != null) {
             return;
         }
-        mIconCache.getTitleAndIcon(info, activityInfo, true /* useLowResIcon */);
+        mIconCache.getTitleAndIcon(info, activityInfo, false /* useLowResIcon */);
         info.sectionName = mIndex.computeSectionName(info.title);
 
         data.add(info);
@@ -145,10 +144,9 @@
     }
 
     public void addPromiseApp(Context context, PackageInstallInfo installInfo) {
-        ApplicationInfo applicationInfo = new PackageManagerHelper(context)
-                .getApplicationInfo(installInfo.packageName, installInfo.user, 0);
         // only if not yet installed
-        if (applicationInfo == null) {
+        if (!new PackageManagerHelper(context)
+                .isAppInstalled(installInfo.packageName, installInfo.user)) {
             AppInfo info = new AppInfo(installInfo);
             mIconCache.getTitleAndIcon(info, info.usingLowResIcon());
             info.sectionName = mIndex.computeSectionName(info.title);
@@ -203,11 +201,16 @@
     /**
      * Add the icons for the supplied apk called packageName.
      */
-    public void addPackage(Context context, String packageName, UserHandle user) {
-        for (LauncherActivityInfo info : context.getSystemService(LauncherApps.class)
-                .getActivityList(packageName, user)) {
+    public List<LauncherActivityInfo> addPackage(
+            Context context, String packageName, UserHandle user) {
+        List<LauncherActivityInfo> activities = context.getSystemService(LauncherApps.class)
+                .getActivityList(packageName, user);
+
+        for (LauncherActivityInfo info : activities) {
             add(new AppInfo(context, info, user), info);
         }
+
+        return activities;
     }
 
     /**
@@ -250,7 +253,8 @@
     /**
      * Add and remove icons for this package which has been updated.
      */
-    public void updatePackage(Context context, String packageName, UserHandle user) {
+    public List<LauncherActivityInfo> updatePackage(
+            Context context, String packageName, UserHandle user) {
         final List<LauncherActivityInfo> matches = context.getSystemService(LauncherApps.class)
                 .getActivityList(packageName, user);
         if (matches.size() > 0) {
@@ -276,7 +280,7 @@
                 } else {
                     Intent launchIntent = AppInfo.makeLaunchIntent(info);
 
-                    mIconCache.getTitleAndIcon(applicationInfo, info, true /* useLowResIcon */);
+                    mIconCache.getTitleAndIcon(applicationInfo, info, false /* useLowResIcon */);
                     applicationInfo.sectionName = mIndex.computeSectionName(applicationInfo.title);
                     applicationInfo.setProgressLevel(
                             PackageManagerHelper.getLoadingProgress(info),
@@ -297,6 +301,8 @@
                 }
             }
         }
+
+        return matches;
     }
 
     /**
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index d1e5017..ad553d5 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -28,7 +28,7 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ItemInfoMatcher;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -123,8 +123,8 @@
     }
 
     public void bindUpdatedWidgets(BgDataModel dataModel) {
-        final ArrayList<WidgetListRowEntry> widgets =
-                dataModel.widgetsModel.getWidgetsList(mApp.getContext());
+        final ArrayList<WidgetsListBaseEntry> widgets =
+                dataModel.widgetsModel.getWidgetsListForPicker(mApp.getContext());
         scheduleCallbackTask(c -> c.bindAllWidgets(widgets));
     }
 
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 2d860a4..1d7d1a2 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -50,7 +50,7 @@
 import com.android.launcher3.util.IntSparseArrayMap;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.ViewOnDrawExecutor;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -467,7 +467,7 @@
         void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets);
         void bindRestoreItemsChange(HashSet<ItemInfo> updates);
         void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher);
-        void bindAllWidgets(ArrayList<WidgetListRowEntry> widgets);
+        void bindAllWidgets(List<WidgetsListBaseEntry> widgets);
         void onPageBoundSynchronously(int page);
         void executeOnNextDraw(ViewOnDrawExecutor executor);
         void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap);
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java
index e8a52bd..804e72e 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTask.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java
@@ -25,7 +25,6 @@
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherSettings.Settings;
@@ -40,6 +39,7 @@
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
 import java.util.ArrayList;
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
index 79467d3..c6c0791 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
@@ -39,7 +39,6 @@
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.graphics.LauncherPreviewRenderer;
@@ -48,6 +47,7 @@
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
 import java.util.ArrayList;
diff --git a/src/com/android/launcher3/model/ItemInstallQueue.java b/src/com/android/launcher3/model/ItemInstallQueue.java
index d09bf81..df8367f 100644
--- a/src/com/android/launcher3/model/ItemInstallQueue.java
+++ b/src/com/android/launcher3/model/ItemInstallQueue.java
@@ -42,7 +42,6 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -52,6 +51,7 @@
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.PersistedItemArray;
 import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 import java.util.HashSet;
 import java.util.List;
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 8438622..f6c7c06 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -45,12 +45,12 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.LongSparseArray;
 import android.util.TimingLogger;
 
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
@@ -87,6 +87,7 @@
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
 import java.util.ArrayList;
@@ -126,7 +127,7 @@
 
     private final UserManagerState mUserManagerState = new UserManagerState();
 
-    protected Map<ComponentKey, AppWidgetProviderInfo> mWidgetProvidersMap;
+    protected final Map<ComponentKey, AppWidgetProviderInfo> mWidgetProvidersMap = new ArrayMap<>();
 
     private boolean mStopped;
 
@@ -191,6 +192,16 @@
             loadWorkspace(allShortcuts);
             logger.addSplit("loadWorkspace");
 
+            // Sanitize data re-syncs widgets/shortcuts based on the workspace loaded from db.
+            // sanitizeData should not be invoked if the workspace is loaded from a db different
+            // from the main db as defined in the invariant device profile.
+            // (e.g. both grid preview and minimal device mode uses a different db)
+            if (mApp.getInvariantDeviceProfile().dbFile.equals(mDbName)) {
+                verifyNotStopped();
+                sanitizeData();
+                logger.addSplit("sanitizeData");
+            }
+
             verifyNotStopped();
             mResults.bindWorkspace();
             logger.addSplit("bindWorkspace");
@@ -273,14 +284,6 @@
                 loadFolderNames();
             }
 
-            // Sanitize data re-syncs widgets/shortcuts based on the workspace loaded from db.
-            // sanitizeData should not be invoked if the workspace is loaded from a db different
-            // from the main db as defined in the invariant device profile.
-            // (e.g. both grid preview and minimal device mode uses a different db)
-            if (mApp.getInvariantDeviceProfile().dbFile.equals(mDbName)) {
-                sanitizeData();
-            }
-
             verifyNotStopped();
             updateHandler.finish();
             logger.addSplit("finish icon update");
@@ -664,12 +667,13 @@
                             final boolean wasProviderReady = !c.hasRestoreFlag(
                                     LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY);
 
-                            if (mWidgetProvidersMap == null) {
-                                mWidgetProvidersMap = WidgetManagerHelper.getAllProvidersMap(
-                                        context);
+                            ComponentKey providerKey = new ComponentKey(component, c.user);
+                            if (!mWidgetProvidersMap.containsKey(providerKey)) {
+                                mWidgetProvidersMap.put(providerKey,
+                                        widgetHelper.findProvider(component, c.user));
                             }
-                            final AppWidgetProviderInfo provider = mWidgetProvidersMap.get(
-                                    new ComponentKey(component, c.user));
+                            final AppWidgetProviderInfo provider =
+                                    mWidgetProvidersMap.get(providerKey);
 
                             final boolean isProviderReady = isValidProvider(provider);
                             if (!isSafeMode && !customWidget &&
@@ -874,10 +878,10 @@
                 }
             }
 
-            // Remove any ghost widgets
-            LauncherSettings.Settings.call(contentResolver,
-                    LauncherSettings.Settings.METHOD_REMOVE_GHOST_WIDGETS);
         }
+        // Remove any ghost widgets
+        LauncherSettings.Settings.call(contentResolver,
+                LauncherSettings.Settings.METHOD_REMOVE_GHOST_WIDGETS);
 
         // Update pinned state of model shortcuts
         mBgDataModel.updateShortcutPinnedState(context);
diff --git a/src/com/android/launcher3/model/ModelDelegate.java b/src/com/android/launcher3/model/ModelDelegate.java
index 92bea5b..13ec1ec 100644
--- a/src/com/android/launcher3/model/ModelDelegate.java
+++ b/src/com/android/launcher3/model/ModelDelegate.java
@@ -27,6 +27,8 @@
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ResourceBasedOverride;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 import java.util.Map;
 
 /**
@@ -89,4 +91,11 @@
     @WorkerThread
     public void destroy() { }
 
+    /**
+     * Add data to a dumpsys request for Launcher (e.g. for bug reports).
+     *
+     * @see com.android.launcher3.Launcher#dump(java.lang.String, java.io.FileDescriptor,
+     *                                          java.io.PrintWriter, java.lang.String[])
+     **/
+    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { }
 }
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index f7b43d6..312435d 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -28,7 +28,6 @@
 import android.util.Log;
 
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetHost;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherProvider;
 import com.android.launcher3.LauncherSettings;
@@ -43,6 +42,7 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.ContentWriter;
 import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
 
 import java.util.ArrayList;
 import java.util.Arrays;
diff --git a/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java b/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java
index e3e8769..c0dc34a 100644
--- a/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.model;
 
-import android.content.ComponentName;
 import android.os.UserHandle;
 
 import com.android.launcher3.LauncherAppState;
@@ -66,8 +65,7 @@
         final ArrayList<WorkspaceItemInfo> updatedWorkspaceItems = new ArrayList<>();
         synchronized (dataModel) {
             dataModel.forAllWorkspaceItemInfos(mUser, si -> {
-                ComponentName cn = si.getTargetComponent();
-                if ((cn != null) && cn.getPackageName().equals(mPackageName)) {
+                if (mPackageName.equals(si.getTargetPackage())) {
                     si.runtimeStatusFlags &= ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
                     si.setProgressLevel(downloadInfo);
                     updatedWorkspaceItems.add(si);
diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
index 8215edd..9889a80 100644
--- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
+++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.model;
 
-import android.content.ComponentName;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 
@@ -72,9 +71,8 @@
         synchronized (dataModel) {
             final HashSet<ItemInfo> updates = new HashSet<>();
             dataModel.forAllWorkspaceItemInfos(mInstallInfo.user, si -> {
-                ComponentName cn = si.getTargetComponent();
-                if (si.hasPromiseIconUi() && (cn != null)
-                        && cn.getPackageName().equals(mInstallInfo.packageName)) {
+                if (si.hasPromiseIconUi()
+                        && mInstallInfo.packageName.equals(si.getTargetPackage())) {
                     int installProgress = mInstallInfo.progress;
 
                     si.setProgressLevel(installProgress, PackageInstallInfo.STATUS_INSTALLING);
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 3275d59..7bfa3ef 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -22,6 +22,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.ShortcutInfo;
 import android.os.UserHandle;
@@ -29,6 +30,7 @@
 import android.util.Log;
 
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.BitmapInfo;
@@ -51,6 +53,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 
@@ -95,6 +98,7 @@
                 ? ItemInfoMatcher.ofUser(mUser) // We want to update all packages for this user
                 : ItemInfoMatcher.ofPackages(packageSet, mUser);
         final HashSet<ComponentName> removedComponents = new HashSet<>();
+        final HashMap<String, List<LauncherActivityInfo>> activitiesLists = new HashMap<>();
 
         switch (mOp) {
             case OP_ADD: {
@@ -104,7 +108,8 @@
                     if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
                         appsList.removePackage(packages[i], mUser);
                     }
-                    appsList.addPackage(context, packages[i], mUser);
+                    activitiesLists.put(
+                            packages[i], appsList.addPackage(context, packages[i], mUser));
                 }
                 flagOp = FlagOp.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE);
                 break;
@@ -115,7 +120,8 @@
                     for (int i = 0; i < N; i++) {
                         if (DEBUG) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]);
                         iconCache.updateIconsForPkg(packages[i], mUser);
-                        appsList.updatePackage(context, packages[i], mUser);
+                        activitiesLists.put(
+                                packages[i], appsList.updatePackage(context, packages[i], mUser));
                         app.getWidgetCache().removePackage(packages[i], mUser);
                     }
                 }
@@ -223,7 +229,8 @@
                                 isTargetValid = context.getSystemService(LauncherApps.class)
                                         .isActivityEnabled(cn, mUser);
                             }
-                            if (si.hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)) {
+                            if (!isTargetValid && si.hasStatusFlag(
+                                    FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)) {
                                 if (updateWorkspaceItemIntent(context, si, packageName)) {
                                     infoUpdated = true;
                                 } else if (si.hasPromiseIconUi()) {
@@ -245,11 +252,19 @@
                             }
                         }
 
-                        if (isNewApkAvailable
-                                && si.itemType == Favorites.ITEM_TYPE_APPLICATION) {
-                            si.setProgressLevel(100, PackageInstallInfo.STATUS_INSTALLED);
-                            iconCache.getTitleAndIcon(si, si.usingLowResIcon());
-                            infoUpdated = true;
+                        if (isNewApkAvailable) {
+                            List<LauncherActivityInfo> activities = activitiesLists.get(
+                                    packageName);
+                            si.setProgressLevel(
+                                    activities == null || activities.isEmpty()
+                                            ? 100
+                                            : PackageManagerHelper.getLoadingProgress(
+                                                    activities.get(0)),
+                                    PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
+                            if (si.itemType == Favorites.ITEM_TYPE_APPLICATION) {
+                                iconCache.getTitleAndIcon(si, si.usingLowResIcon());
+                                infoUpdated = true;
+                            }
                         }
 
                         int oldRuntimeFlags = si.runtimeStatusFlags;
@@ -341,6 +356,11 @@
      */
     private boolean updateWorkspaceItemIntent(Context context,
             WorkspaceItemInfo si, String packageName) {
+        if (si.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+            // Do not update intent for deep shortcuts as they contain additional information
+            // about the shortcut.
+            return false;
+        }
         // Try to find the best match activity.
         Intent intent = new PackageManagerHelper(context).getAppLaunchIntent(packageName, mUser);
         if (intent != null) {
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java
index 6fedad1..4296d32 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.java
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.java
@@ -25,6 +25,7 @@
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.PackageManagerHelper;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -66,6 +67,14 @@
         }
 
         if (!matchingWorkspaceItems.isEmpty()) {
+            if (mShortcuts.isEmpty()) {
+                // Verify that the app is indeed installed.
+                if (!new PackageManagerHelper(app.getContext())
+                        .isAppInstalled(mPackageName, mUser)) {
+                    // App is not installed, ignoring package events
+                    return;
+                }
+            }
             // Update the workspace to reflect the changes to updated shortcuts residing on it.
             List<String> allLauncherKnownIds = matchingWorkspaceItems.stream()
                     .map(WorkspaceItemInfo::getDeepShortcutId)
diff --git a/src/com/android/launcher3/model/WidgetItem.java b/src/com/android/launcher3/model/WidgetItem.java
index 37c089e..97071bb 100644
--- a/src/com/android/launcher3/model/WidgetItem.java
+++ b/src/com/android/launcher3/model/WidgetItem.java
@@ -4,11 +4,11 @@
 import android.content.pm.PackageManager;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 /**
  * An wrapper over various items displayed in a widget picker,
@@ -43,4 +43,20 @@
         activityInfo = info;
         spanX = spanY = 1;
     }
+
+    /**
+     * Returns {@code true} if this {@link WidgetItem} has the same type as the given
+     * {@code otherItem}.
+     *
+     * For example, both items are widgets or both items are shortcuts.
+     */
+    public boolean hasSameType(WidgetItem otherItem) {
+        if (widgetInfo != null && otherItem.widgetInfo != null) {
+            return true;
+        }
+        if (activityInfo != null && otherItem.activityInfo != null) {
+            return true;
+        }
+        return false;
+    }
 }
diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java
index cc783f7..cd2ef35 100644
--- a/src/com/android/launcher3/model/data/FolderInfo.java
+++ b/src/com/android/launcher3/model/data/FolderInfo.java
@@ -34,6 +34,7 @@
 import com.android.launcher3.folder.FolderNameInfos;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logger.LauncherAtom.Attribute;
+import com.android.launcher3.logger.LauncherAtom.FolderIcon;
 import com.android.launcher3.logger.LauncherAtom.FromState;
 import com.android.launcher3.logger.LauncherAtom.ToState;
 import com.android.launcher3.model.ModelWriter;
@@ -208,8 +209,13 @@
 
     @Override
     public LauncherAtom.ItemInfo buildProto(FolderInfo fInfo) {
+        FolderIcon.Builder folderIcon = FolderIcon.newBuilder()
+                .setCardinality(contents.size());
+        if (LabelState.SUGGESTED.equals(getLabelState())) {
+            folderIcon.setLabelInfo(title.toString());
+        }
         return getDefaultItemInfoBuilder()
-                .setFolderIcon(LauncherAtom.FolderIcon.newBuilder().setCardinality(contents.size()))
+                .setFolderIcon(folderIcon)
                 .setRank(rank)
                 .setAttribute(getLabelState().mLogAttribute)
                 .setContainerInfo(getContainerInfo())
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index b11b419..e54f1e7 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -26,6 +26,7 @@
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_SHORTCUTS;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_TASKSWITCHER;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
+import static com.android.launcher3.LauncherSettings.Favorites.EXTENDED_CONTAINERS;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
@@ -54,6 +55,7 @@
 import com.android.launcher3.logger.LauncherAtom.Shortcut;
 import com.android.launcher3.logger.LauncherAtom.ShortcutsContainer;
 import com.android.launcher3.logger.LauncherAtom.TaskSwitcherContainer;
+import com.android.launcher3.logger.LauncherAtomExtensions.ExtendedContainers;
 import com.android.launcher3.model.ModelWriter;
 import com.android.launcher3.util.ContentWriter;
 
@@ -182,6 +184,24 @@
         return Optional.ofNullable(getIntent()).map(Intent::getComponent).orElse(mComponentName);
     }
 
+    /**
+     * Returns this item's package name.
+     *
+     * Prioritizes the component package name, then uses the intent package name as a fallback.
+     * This ensures deep shortcuts are supported.
+     */
+    @Nullable
+    public String getTargetPackage() {
+        ComponentName component = getTargetComponent();
+        Intent intent = getIntent();
+
+        return component != null
+                ? component.getPackageName()
+                : intent != null
+                        ? intent.getPackage()
+                        : null;
+    }
+
     public void writeToValues(ContentWriter writer) {
         writer.put(LauncherSettings.Favorites.ITEM_TYPE, itemType)
                 .put(LauncherSettings.Favorites.CONTAINER, container)
@@ -225,7 +245,7 @@
     protected String dumpProperties() {
         return "id=" + id
                 + " type=" + LauncherSettings.Favorites.itemTypeToString(itemType)
-                + " container=" + LauncherSettings.Favorites.containerToString(container)
+                + " container=" + getContainerInfo()
                 + " targetComponent=" + getTargetComponent()
                 + " screen=" + screenId
                 + " cell(" + cellX + "," + cellY + ")"
@@ -352,7 +372,10 @@
         return itemBuilder;
     }
 
-    protected ContainerInfo getContainerInfo() {
+    /**
+     * Returns {@link ContainerInfo} used when logging this item.
+     */
+    public ContainerInfo getContainerInfo() {
         switch (container) {
             case CONTAINER_HOTSEAT:
                 return ContainerInfo.newBuilder()
@@ -400,12 +423,23 @@
                 return ContainerInfo.newBuilder()
                         .setTaskSwitcherContainer(TaskSwitcherContainer.getDefaultInstance())
                         .build();
-
+            case EXTENDED_CONTAINERS:
+                return ContainerInfo.newBuilder()
+                        .setExtendedContainers(getExtendedContainer())
+                        .build();
         }
         return ContainerInfo.getDefaultInstance();
     }
 
     /**
+     * Returns non-AOSP container wrapped by {@link ExtendedContainers} object. Should be overridden
+     * by build variants.
+     */
+    protected ExtendedContainers getExtendedContainer() {
+        return ExtendedContainers.getDefaultInstance();
+    }
+
+    /**
      * Returns shallow copy of the object.
      */
     public ItemInfo makeShallowCopy() {
diff --git a/src/com/android/launcher3/model/data/PackageItemInfo.java b/src/com/android/launcher3/model/data/PackageItemInfo.java
index b70d0d4..7617d7e 100644
--- a/src/com/android/launcher3/model/data/PackageItemInfo.java
+++ b/src/com/android/launcher3/model/data/PackageItemInfo.java
@@ -60,6 +60,6 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(packageName);
+        return Objects.hash(packageName, user);
     }
 }
diff --git a/src/com/android/launcher3/model/data/SearchActionItemInfo.java b/src/com/android/launcher3/model/data/SearchActionItemInfo.java
index 36afe50..b3057d5 100644
--- a/src/com/android/launcher3/model/data/SearchActionItemInfo.java
+++ b/src/com/android/launcher3/model/data/SearchActionItemInfo.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.model.data;
 
+import static com.android.launcher3.LauncherSettings.Favorites.EXTENDED_CONTAINERS;
+
 import android.app.PendingIntent;
 import android.content.Intent;
 import android.graphics.drawable.Icon;
@@ -23,6 +25,9 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.logger.LauncherAtom.ItemInfo;
+import com.android.launcher3.logger.LauncherAtom.SearchActionItem;
+
 /**
  * Represents a SearchAction with in launcher
  */
@@ -30,21 +35,26 @@
 
     public static final int FLAG_SHOULD_START = 1 << 1;
     public static final int FLAG_SHOULD_START_FOR_RESULT = FLAG_SHOULD_START | 1 << 2;
-    public static final int FLAG_BADGE_FROM_ICON = 1 << 3;
+    public static final int FLAG_BADGE_WITH_PACKAGE = 1 << 3;
     public static final int FLAG_PRIMARY_ICON_FROM_TITLE = 1 << 4;
+    public static final int FLAG_BADGE_WITH_COMPONENT_NAME = 1 << 5;
 
     private final String mFallbackPackageName;
     private int mFlags = 0;
     private final Icon mIcon;
 
+    // If true title does not contain any personal info and eligible for logging.
+    private final boolean mIsPersonalTitle;
     private Intent mIntent;
 
     private PendingIntent mPendingIntent;
 
     public SearchActionItemInfo(Icon icon, String packageName, UserHandle user,
-            CharSequence title) {
+            CharSequence title, boolean isPersonalTitle) {
+        mIsPersonalTitle = isPersonalTitle;
         this.user = user == null ? Process.myUserHandle() : user;
         this.title = title;
+        this.container = EXTENDED_CONTAINERS;
         mFallbackPackageName = packageName;
         mIcon = icon;
     }
@@ -55,6 +65,8 @@
         mFallbackPackageName = info.mFallbackPackageName;
         mFlags = info.mFlags;
         title = info.title;
+        this.container = EXTENDED_CONTAINERS;
+        this.mIsPersonalTitle = info.mIsPersonalTitle;
     }
 
     /**
@@ -108,4 +120,18 @@
     public ItemInfoWithIcon clone() {
         return new SearchActionItemInfo(this);
     }
+
+    @Override
+    public ItemInfo buildProto(FolderInfo fInfo) {
+        SearchActionItem.Builder itemBuilder = SearchActionItem.newBuilder()
+                .setPackageName(mFallbackPackageName);
+
+        if (!mIsPersonalTitle) {
+            itemBuilder.setTitle(title.toString());
+        }
+        return getDefaultItemInfoBuilder()
+                .setSearchActionItem(itemBuilder)
+                .setContainerInfo(getContainerInfo())
+                .build();
+    }
 }
diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java
index 059ad18..2905dc3 100644
--- a/src/com/android/launcher3/notification/NotificationListener.java
+++ b/src/com/android/launcher3/notification/NotificationListener.java
@@ -16,9 +16,9 @@
 
 package com.android.launcher3.notification;
 
+import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
 
 import android.annotation.TargetApi;
 import android.app.Notification;
@@ -37,8 +37,8 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
+import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.SecureSettingsObserver;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -81,7 +81,8 @@
     /** The last notification key that was dismissed from launcher UI */
     private String mLastKeyDismissedByLauncher;
 
-    private SecureSettingsObserver mNotificationDotsObserver;
+    private SettingsCache mSettingsCache;
+    private SettingsCache.OnChangeListener mNotificationSettingsChangedListener;
 
     public NotificationListener() {
         mWorkerHandler = new Handler(MODEL_EXECUTOR.getLooper(), this::handleWorkerMessage);
@@ -207,10 +208,12 @@
         super.onListenerConnected();
         sIsConnected = true;
 
-        mNotificationDotsObserver =
-                newNotificationSettingsObserver(this, this::onNotificationSettingsChanged);
-        mNotificationDotsObserver.register();
-        mNotificationDotsObserver.dispatchOnChange();
+        // Register an observer to rebind the notification listener when dots are re-enabled.
+        mSettingsCache = SettingsCache.INSTANCE.get(this);
+        mNotificationSettingsChangedListener = this::onNotificationSettingsChanged;
+        mSettingsCache.register(NOTIFICATION_BADGING_URI,
+                mNotificationSettingsChangedListener);
+        mSettingsCache.dispatchOnChange(NOTIFICATION_BADGING_URI);
 
         onNotificationFullRefresh();
     }
@@ -229,7 +232,7 @@
     public void onListenerDisconnected() {
         super.onListenerDisconnected();
         sIsConnected = false;
-        mNotificationDotsObserver.unregister();
+        mSettingsCache.unregister(NOTIFICATION_BADGING_URI, mNotificationSettingsChangedListener);
         onNotificationFullRefresh();
     }
 
diff --git a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
index 408796f..6189dc9 100644
--- a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
+++ b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
@@ -268,7 +268,9 @@
         } else {
             lp.leftMargin = lp.rightMargin = 0;
             lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
-            lp.bottomMargin = grid.hotseatBarSizePx + insets.bottom;
+            lp.bottomMargin = grid.isTaskbarPresent
+                    ? grid.workspacePadding.bottom + insets.bottom
+                    : grid.hotseatBarSizePx + insets.bottom;
         }
         setLayoutParams(lp);
     }
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index fa25114..0091af1 100644
--- a/src/com/android/launcher3/pm/InstallSessionHelper.java
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -217,8 +217,8 @@
                 && sessionInfo.getAppIcon() != null
                 && !TextUtils.isEmpty(sessionInfo.getAppLabel())
                 && !promiseIconAddedForId(sessionInfo.getSessionId())
-                && new PackageManagerHelper(mAppContext).getApplicationInfo(
-                        sessionInfo.getAppPackageName(), getUserHandle(sessionInfo), 0) == null) {
+                && !new PackageManagerHelper(mAppContext).isAppInstalled(
+                        sessionInfo.getAppPackageName(), getUserHandle(sessionInfo))) {
             ItemInstallQueue.INSTANCE.get(mAppContext)
                     .queueItem(sessionInfo.getAppPackageName(), getUserHandle(sessionInfo));
 
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 90285c4..5a34d2a 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -26,11 +26,8 @@
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.res.Resources;
-import android.graphics.CornerPathEffect;
 import android.graphics.Outline;
-import android.graphics.Paint;
 import android.graphics.Rect;
-import android.graphics.drawable.ShapeDrawable;
 import android.util.AttributeSet;
 import android.util.Pair;
 import android.view.Gravity;
@@ -40,6 +37,8 @@
 import android.view.ViewOutlineProvider;
 import android.widget.FrameLayout;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.InsettableFrameLayout;
@@ -49,7 +48,6 @@
 import com.android.launcher3.anim.RevealOutlineAnimation;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.graphics.TriangleShape;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer;
 
@@ -70,7 +68,11 @@
     protected final T mLauncher;
     protected final boolean mIsRtl;
 
-    private final int mArrowOffset;
+    private final int mArrowOffsetVertical;
+    private final int mArrowOffsetHorizontal;
+    private final int mArrowWidth;
+    private final int mArrowHeight;
+    private final int mArrowPointRadius;
     private final View mArrow;
 
     protected boolean mIsLeftAligned;
@@ -82,6 +84,8 @@
     private final Rect mStartRect = new Rect();
     private final Rect mEndRect = new Rect();
 
+    private Runnable mOnCloseCallback = () -> { };
+
     public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         mInflater = LayoutInflater.from(context);
@@ -99,11 +103,14 @@
 
         // Initialize arrow view
         final Resources resources = getResources();
-        final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
-        final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
+        mArrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
+        mArrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
         mArrow = new View(context);
-        mArrow.setLayoutParams(new DragLayer.LayoutParams(arrowWidth, arrowHeight));
-        mArrowOffset = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset);
+        mArrow.setLayoutParams(new DragLayer.LayoutParams(mArrowWidth, mArrowHeight));
+        mArrowOffsetVertical = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset);
+        mArrowOffsetHorizontal = resources.getDimensionPixelSize(
+                R.dimen.popup_arrow_horizontal_center_offset) - (mArrowWidth / 2);
+        mArrowPointRadius = resources.getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
     }
 
     public ArrowPopup(Context context, AttributeSet attrs) {
@@ -196,48 +203,33 @@
         orientAboutObject();
     }
 
-    private void addArrow() {
-        final Resources res = getResources();
-        final int arrowCenterOffset = res.getDimensionPixelSize(isAlignedWithStart()
-                ? R.dimen.popup_arrow_horizontal_center_start
-                : R.dimen.popup_arrow_horizontal_center_end);
-        final int halfArrowWidth = res.getDimensionPixelSize(R.dimen.popup_arrow_width) / 2;
-        getPopupContainer().addView(mArrow);
-        DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams();
+    private int getArrowLeft() {
         if (mIsLeftAligned) {
-            mArrow.setX(getX() + arrowCenterOffset - halfArrowWidth);
-        } else {
-            mArrow.setX(getX() + getMeasuredWidth() - arrowCenterOffset - halfArrowWidth);
+            return mArrowOffsetHorizontal;
         }
+        return getMeasuredWidth() - mArrowOffsetHorizontal - mArrowWidth;
+    }
+
+    private void addArrow() {
+        getPopupContainer().addView(mArrow);
+        mArrow.setX(getX() + getArrowLeft());
 
         if (Gravity.isVertical(mGravity)) {
             // This is only true if there wasn't room for the container next to the icon,
             // so we centered it instead. In that case we don't want to showDefaultOptions the arrow.
             mArrow.setVisibility(INVISIBLE);
         } else {
-            ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
-                    arrowLp.width, arrowLp.height, !mIsAboveIcon));
-            Paint arrowPaint = arrowDrawable.getPaint();
-            arrowPaint.setColor(Themes.getAttrColor(getContext(), R.attr.popupColorPrimary));
-            // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
-            int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
-            arrowPaint.setPathEffect(new CornerPathEffect(radius));
-            mArrow.setBackground(arrowDrawable);
-            // Clip off the part of the arrow that is underneath the popup.
-            if (mIsAboveIcon) {
-                mArrow.setClipBounds(new Rect(0, -mArrowOffset, arrowLp.width, arrowLp.height));
-            } else {
-                mArrow.setClipBounds(new Rect(0, 0, arrowLp.width, arrowLp.height + mArrowOffset));
-            }
+            mArrow.setBackground(new RoundedArrowDrawable(
+                    mArrowWidth, mArrowHeight, mArrowPointRadius,
+                    mOutlineRadius, getMeasuredWidth(), getMeasuredHeight(),
+                    mArrowOffsetHorizontal, -mArrowOffsetVertical,
+                    !mIsAboveIcon, mIsLeftAligned,
+                    Themes.getAttrColor(getContext(), R.attr.popupColorPrimary)));
             mArrow.setElevation(getElevation());
         }
 
-        mArrow.setPivotX(arrowLp.width / 2);
-        mArrow.setPivotY(mIsAboveIcon ? arrowLp.height : 0);
-    }
-
-    protected boolean isAlignedWithStart() {
-        return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
+        mArrow.setPivotX(mArrowWidth / 2.0f);
+        mArrow.setPivotY(mIsAboveIcon ? mArrowHeight : 0);
     }
 
     /**
@@ -270,8 +262,9 @@
      */
     private void orientAboutObject(boolean allowAlignLeft, boolean allowAlignRight) {
         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
         int width = getMeasuredWidth();
-        int extraVerticalSpace = mArrow.getLayoutParams().height + mArrowOffset
+        int extraVerticalSpace = mArrowHeight + mArrowOffsetVertical
                 + getResources().getDimensionPixelSize(R.dimen.popup_vertical_padding);
         int height = getMeasuredHeight() + extraVerticalSpace;
 
@@ -287,22 +280,7 @@
 
         // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
         int iconWidth = mTempRect.width();
-        Resources resources = getResources();
-        int xOffset;
-        if (isAlignedWithStart()) {
-            // Aligning with the shortcut icon.
-            int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
-            int shortcutPaddingStart = resources.getDimensionPixelSize(
-                    R.dimen.popup_padding_start);
-            xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
-        } else {
-            // Aligning with the drag handle.
-            int shortcutDragHandleWidth = resources.getDimensionPixelSize(
-                    R.dimen.deep_shortcut_drag_handle_size);
-            int shortcutPaddingEnd = resources.getDimensionPixelSize(
-                    R.dimen.popup_padding_end);
-            xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
-        }
+        int xOffset = iconWidth / 2 - mArrowOffsetHorizontal - mArrowWidth / 2;
         x += mIsLeftAligned ? xOffset : -xOffset;
 
         // Check whether we can still align as we originally wanted, now that we've calculated x.
@@ -371,12 +349,14 @@
         FrameLayout.LayoutParams arrowLp = (FrameLayout.LayoutParams) mArrow.getLayoutParams();
         if (mIsAboveIcon) {
             arrowLp.gravity = lp.gravity = Gravity.BOTTOM;
-            lp.bottomMargin = getPopupContainer().getHeight() - y - getMeasuredHeight() - insets.top;
-            arrowLp.bottomMargin = lp.bottomMargin - arrowLp.height - mArrowOffset - insets.bottom;
+            lp.bottomMargin =
+                    getPopupContainer().getHeight() - y - getMeasuredHeight() - insets.top;
+            arrowLp.bottomMargin =
+                    lp.bottomMargin - arrowLp.height - mArrowOffsetVertical - insets.bottom;
         } else {
             arrowLp.gravity = lp.gravity = Gravity.TOP;
             lp.topMargin = y + insets.top;
-            arrowLp.topMargin = lp.topMargin - insets.top - arrowLp.height - mArrowOffset;
+            arrowLp.topMargin = lp.topMargin - insets.top - arrowLp.height - mArrowOffsetVertical;
         }
     }
 
@@ -525,22 +505,13 @@
     protected void onCreateCloseAnimation(AnimatorSet anim) { }
 
     private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
-        Resources res = getResources();
-        int arrowCenterX = res.getDimensionPixelSize(mIsLeftAligned ^ mIsRtl ?
-                R.dimen.popup_arrow_horizontal_center_start:
-                R.dimen.popup_arrow_horizontal_center_end);
-        int halfArrowWidth = res.getDimensionPixelSize(R.dimen.popup_arrow_width) / 2;
-        float arrowCornerRadius = res.getDimension(R.dimen.popup_arrow_corner_radius);
-        if (!mIsLeftAligned) {
-            arrowCenterX = getMeasuredWidth() - arrowCenterX;
-        }
+        int arrowLeft = getArrowLeft();
         int arrowCenterY = mIsAboveIcon ? getMeasuredHeight() : 0;
 
-        mStartRect.set(arrowCenterX - halfArrowWidth, arrowCenterY, arrowCenterX + halfArrowWidth,
-                arrowCenterY);
+        mStartRect.set(arrowLeft, arrowCenterY, arrowLeft + mArrowWidth, arrowCenterY);
 
-        return new RoundedRectRevealOutlineProvider
-                (arrowCornerRadius, mOutlineRadius, mStartRect, mEndRect);
+        return new RoundedRectRevealOutlineProvider(
+                mArrowPointRadius, mOutlineRadius, mStartRect, mEndRect);
     }
 
     /**
@@ -555,6 +526,14 @@
         mDeferContainerRemoval = false;
         getPopupContainer().removeView(this);
         getPopupContainer().removeView(mArrow);
+        mOnCloseCallback.run();
+    }
+
+    /**
+     * Callback to be called when the popup is closed
+     */
+    public void setOnCloseCallback(@NonNull Runnable callback) {
+        mOnCloseCallback = callback;
     }
 
     protected BaseDragLayer getPopupContainer() {
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 59930ff..a1ba747 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -206,6 +206,7 @@
                         .filter(Objects::nonNull)
                         .collect(Collectors.toList()));
         launcher.refreshAndBindWidgetsForPackageUser(PackageUserKey.fromItemInfo(item));
+        container.requestFocus();
         return container;
     }
 
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index 76048ba..7780894 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -31,10 +31,10 @@
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.ShortcutUtil;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
 
 import java.io.PrintWriter;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -58,8 +58,11 @@
     private HashMap<ComponentKey, Integer> mDeepShortcutMap = new HashMap<>();
     /** Maps packages to their DotInfo's . */
     private Map<PackageUserKey, DotInfo> mPackageUserToDotInfos = new HashMap<>();
-    /** Maps packages to their Widgets */
-    private ArrayList<WidgetListRowEntry> mAllWidgets = new ArrayList<>();
+
+    /** All installed widgets. */
+    private List<WidgetsListBaseEntry> mAllWidgets = List.of();
+    /** Widgets that can be recommended to the users. */
+    private List<ItemInfo> mRecommendedWidgets = List.of();
 
     private PopupDataChangeListener mChangeListener = PopupDataChangeListener.INSTANCE;
 
@@ -187,7 +190,16 @@
         notificationListener.cancelNotificationFromLauncher(notificationKey);
     }
 
-    public void setAllWidgets(ArrayList<WidgetListRowEntry> allWidgets) {
+    /**
+     * Sets a list of recommended widgets ordered by their order of appearance in the widgets
+     * recommendation UI.
+     */
+    public void setRecommendedWidgets(List<ItemInfo> recommendedWidgets) {
+        mRecommendedWidgets = recommendedWidgets;
+        mChangeListener.onRecommendedWidgetsBound();
+    }
+
+    public void setAllWidgets(List<WidgetsListBaseEntry> allWidgets) {
         mAllWidgets = allWidgets;
         mChangeListener.onWidgetsBound();
     }
@@ -196,14 +208,30 @@
         mChangeListener = listener == null ? PopupDataChangeListener.INSTANCE : listener;
     }
 
-    public ArrayList<WidgetListRowEntry> getAllWidgets() {
+    public List<WidgetsListBaseEntry> getAllWidgets() {
         return mAllWidgets;
     }
 
+    /** Returns a list of recommended widgets. */
+    public List<WidgetItem> getRecommendedWidgets() {
+        HashMap<ComponentKey, WidgetItem> allWidgetItems = new HashMap<>();
+        mAllWidgets.stream()
+                .filter(entry -> entry instanceof WidgetsListContentEntry)
+                .forEach(entry -> ((WidgetsListContentEntry) entry).mWidgets
+                        .forEach(widget -> allWidgetItems.put(
+                                new ComponentKey(widget.componentName, widget.user), widget)));
+        return mRecommendedWidgets.stream()
+                .map(recommendedWidget -> allWidgetItems.get(
+                        new ComponentKey(recommendedWidget.getTargetComponent(),
+                                recommendedWidget.user)))
+                .collect(Collectors.toList());
+    }
+
     public List<WidgetItem> getWidgetsForPackageUser(PackageUserKey packageUserKey) {
         return mAllWidgets.stream()
-                .filter(row -> row.pkgItem.packageName.equals(packageUserKey.mPackageName))
-                .flatMap(row -> row.widgets.stream())
+                .filter(row -> row instanceof WidgetsListContentEntry
+                        && row.mPkgItem.packageName.equals(packageUserKey.mPackageName))
+                .flatMap(row -> ((WidgetsListContentEntry) row).mWidgets.stream())
                 .filter(widget -> packageUserKey.mUser.equals(widget.user))
                 .collect(Collectors.toList());
     }
@@ -243,5 +271,8 @@
         default void trimNotifications(Map<PackageUserKey, DotInfo> updatedDots) { }
 
         default void onWidgetsBound() { }
+
+        /** A callback to get notified when recommended widgets are bound. */
+        default void onRecommendedWidgetsBound() { }
     }
 }
diff --git a/src/com/android/launcher3/popup/RoundedArrowDrawable.java b/src/com/android/launcher3/popup/RoundedArrowDrawable.java
new file mode 100644
index 0000000..e662d5c
--- /dev/null
+++ b/src/com/android/launcher3/popup/RoundedArrowDrawable.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2021 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.popup;
+
+import static java.lang.Math.atan;
+import static java.lang.Math.cos;
+import static java.lang.Math.sin;
+import static java.lang.Math.toDegrees;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+
+/**
+ * A drawable for a very specific purpose. Used for the caret arrow on a rounded rectangle popup
+ * bubble.
+ * Draws a triangle with one rounded tip, the opposite edge is clipped by the body of the popup
+ * so there is no overlap when drawing them together.
+ */
+public class RoundedArrowDrawable extends Drawable {
+
+    private final Path mPath;
+    private final Paint mPaint;
+
+    /**
+     * Default constructor.
+     *
+     * @param width of the arrow.
+     * @param height of the arrow.
+     * @param radius of the tip of the arrow.
+     * @param popupRadius of the rect to clip this by.
+     * @param popupWidth of the rect to clip this by.
+     * @param popupHeight of the rect to clip this by.
+     * @param arrowOffsetX from the edge of the popup to the arrow.
+     * @param arrowOffsetY how much the arrow will overlap the popup.
+     * @param isPointingUp or not.
+     * @param leftAligned or false for right aligned.
+     * @param color to draw the triangle.
+     */
+    public RoundedArrowDrawable(float width, float height, float radius, float popupRadius,
+            float popupWidth, float popupHeight,
+            float arrowOffsetX, float arrowOffsetY, boolean isPointingUp, boolean leftAligned,
+            int color) {
+        mPath = new Path();
+        mPaint = new Paint();
+        mPaint.setColor(color);
+        mPaint.setStyle(Paint.Style.FILL);
+        mPaint.setAntiAlias(true);
+
+        // Make the drawable with the triangle pointing down and positioned on the left..
+        addDownPointingRoundedTriangleToPath(width, height, radius, mPath);
+        clipPopupBodyFromPath(popupRadius, popupWidth, popupHeight, arrowOffsetX, arrowOffsetY,
+                mPath);
+
+        // ... then flip it horizontal or vertical based on where it will be used.
+        Matrix pathTransform = new Matrix();
+        pathTransform.setScale(
+                leftAligned ? 1 : -1, isPointingUp ? -1 : 1, width * 0.5f, height * 0.5f);
+        mPath.transform(pathTransform);
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        canvas.drawPath(mPath, mPaint);
+    }
+
+    @Override
+    public void getOutline(Outline outline) {
+        outline.setPath(mPath);
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    @Override
+    public void setAlpha(int i) {
+        mPaint.setAlpha(i);
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter colorFilter) {
+        mPaint.setColorFilter(colorFilter);
+    }
+
+    private static void addDownPointingRoundedTriangleToPath(float width, float height,
+            float radius, Path path) {
+        // Calculated for the arrow pointing down, will be flipped later if needed.
+
+        // Theta is half of the angle inside the triangle tip
+        float tanTheta = width / (2.0f * height);
+        float theta = (float) atan(tanTheta);
+
+        // Some trigonometry to find the center of the circle for the rounded tip
+        float roundedPointCenterY = (float) (height - (radius / sin(theta)));
+
+        // p is the distance along the triangle side to the intersection with the point circle
+        float p = radius / tanTheta;
+        float lineRoundPointIntersectFromCenter = (float) (p * sin(theta));
+        float lineRoundPointIntersectFromTop = (float) (height - (p * cos(theta)));
+
+        float centerX = width / 2.0f;
+        float thetaDeg = (float) toDegrees(theta);
+
+        path.reset();
+        path.moveTo(0, 0);
+        // Draw the top
+        path.lineTo(width, 0);
+        // Draw the right side up to the circle intersection
+        path.lineTo(
+                centerX + lineRoundPointIntersectFromCenter,
+                lineRoundPointIntersectFromTop);
+        // Draw the rounded point
+        path.arcTo(
+                centerX - radius,
+                roundedPointCenterY - radius,
+                centerX + radius,
+                roundedPointCenterY + radius,
+                thetaDeg,
+                180 - (2 * thetaDeg),
+                false);
+        // Draw the left edge to close
+        path.lineTo(0, 0);
+        path.close();
+    }
+
+    private static void clipPopupBodyFromPath(float popupRadius, float popupWidth,
+            float popupHeight, float arrowOffsetX, float arrowOffsetY, Path path) {
+        // Make a path that is used to clip the triangle, this represents the body of the popup
+        Path clipPiece = new Path();
+        clipPiece.addRoundRect(
+                0, 0, popupWidth, popupHeight,
+                popupRadius, popupRadius, Path.Direction.CW);
+        // clipping is performed as if the arrow is pointing down and positioned on the left, the
+        // resulting path will be flipped as needed later.
+        // The extra 0.5 in the vertical offset is to close the gap between this anti-aliased object
+        // and the anti-aliased body of the popup.
+        clipPiece.offset(-arrowOffsetX, -popupHeight + arrowOffsetY - 0.5f);
+        path.op(clipPiece, Path.Op.DIFFERENCE);
+    }
+}
diff --git a/src/com/android/launcher3/qsb/QsbContainerView.java b/src/com/android/launcher3/qsb/QsbContainerView.java
index 289e0d8..a191df4 100644
--- a/src/com/android/launcher3/qsb/QsbContainerView.java
+++ b/src/com/android/launcher3/qsb/QsbContainerView.java
@@ -20,6 +20,8 @@
 import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
 import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_PROVIDER;
 
+import static com.android.launcher3.Utilities.ATLEAST_S;
+
 import android.app.Activity;
 import android.app.Fragment;
 import android.app.SearchManager;
@@ -34,6 +36,7 @@
 import android.os.Bundle;
 import android.provider.Settings;
 import android.util.AttributeSet;
+import android.util.SizeF;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -50,6 +53,8 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.FragmentWithPreview;
 
+import java.util.ArrayList;
+
 /**
  * A frame layout which contains a QSB. This internally uses fragment to bind the view, which
  * allows it to contain the logic for {@link Fragment#startActivityForResult(Intent, int)}.
@@ -294,12 +299,16 @@
             InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext());
 
             Bundle opts = new Bundle();
-            Rect size = AppWidgetResizeFrame.getWidgetSizeRanges(getContext(),
-                    idp.numColumns, 1, null);
+            ArrayList<SizeF> sizes = AppWidgetResizeFrame
+                    .getWidgetSizes(getContext(), idp.numColumns, 1);
+            Rect size = AppWidgetResizeFrame.getMinMaxSizes(sizes, null /* outRect */);
             opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, size.left);
             opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, size.top);
             opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, size.right);
             opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, size.bottom);
+            if (ATLEAST_S) {
+                opts.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, sizes);
+            }
             return opts;
         }
 
diff --git a/src/com/android/launcher3/recyclerview/ViewHolderBinder.java b/src/com/android/launcher3/recyclerview/ViewHolderBinder.java
new file mode 100644
index 0000000..4653774
--- /dev/null
+++ b/src/com/android/launcher3/recyclerview/ViewHolderBinder.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 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.recyclerview;
+
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+/**
+ * Creates and populates views with data
+ *
+ * @param <T> A data model which is used to populate the {@link ViewHolder}.
+ * @param <V> A subclass of {@link ViewHolder} which holds references to views.
+ */
+public interface ViewHolderBinder<T, V extends ViewHolder> {
+    /**
+     * Creates a new view, and attach it to the parent {@link ViewGroup}. Then, populates UI
+     * references in a {@link ViewHolder}.
+     */
+    V newViewHolder(ViewGroup parent);
+
+    /** Populate UI references in {@link ViewHolder} with data. */
+    void bindViewHolder(V viewHolder, T data);
+
+    /**
+     * Called when the view is recycled. Views are recycled in batches once they are sufficiently
+     * far off screen that it is unlikely the user will scroll back to them soon. Optionally
+     * override this to free expensive resources.
+     */
+    default void unbindViewHolder(V viewHolder) {}
+}
diff --git a/src/com/android/launcher3/allapps/search/SearchAlgorithm.java b/src/com/android/launcher3/search/SearchAlgorithm.java
similarity index 76%
rename from src/com/android/launcher3/allapps/search/SearchAlgorithm.java
rename to src/com/android/launcher3/search/SearchAlgorithm.java
index c409b1c..1665354 100644
--- a/src/com/android/launcher3/allapps/search/SearchAlgorithm.java
+++ b/src/com/android/launcher3/search/SearchAlgorithm.java
@@ -13,17 +13,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.launcher3.allapps.search;
+package com.android.launcher3.search;
 
 /**
  * An interface for handling search.
+ *
+ * @param <T> Search Result type
  */
-public interface SearchAlgorithm {
+public interface SearchAlgorithm<T> {
 
     /**
-     * Performs search and sends the result to the callback.
+     * Performs search and sends the result to {@link SearchCallback}.
      */
-    void doSearch(String query, AllAppsSearchBarController.Callbacks callback);
+    void doSearch(String query, SearchCallback<T> callback);
 
     /**
      * Cancels any active request.
diff --git a/src/com/android/launcher3/search/SearchCallback.java b/src/com/android/launcher3/search/SearchCallback.java
new file mode 100644
index 0000000..5796116
--- /dev/null
+++ b/src/com/android/launcher3/search/SearchCallback.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.search;
+
+import java.util.ArrayList;
+
+/**
+ * An interface for receiving search results.
+ *
+ * @param <T> Search Result type
+ */
+public interface SearchCallback<T> {
+
+    /**
+     * Called when the search from primary source is complete.
+     *
+     * @param items list of search results
+     */
+    void onSearchResult(String query, ArrayList<T> items);
+
+    /**
+     * Called when the search from secondary source is complete.
+     *
+     * @param items list of search results
+     */
+    void onAppendSearchResult(String query, ArrayList<T> items);
+
+    /**
+     * Called when the search results should be cleared.
+     */
+    void clearSearchResult();
+}
+
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index 0266345..0e8d4ae 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -46,7 +46,7 @@
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.ViewOnDrawExecutor;
 import com.android.launcher3.views.BaseDragLayer;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -232,7 +232,7 @@
     public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { }
 
     @Override
-    public void bindAllWidgets(ArrayList<WidgetListRowEntry> widgets) { }
+    public void bindAllWidgets(List<WidgetsListBaseEntry> widgets) { }
 
     @Override
     public void onPageBoundSynchronously(int page) { }
diff --git a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
index 47d214d..c9fb956 100644
--- a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
+++ b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
@@ -15,7 +15,10 @@
  */
 package com.android.launcher3.settings;
 
+import static android.content.pm.PackageManager.GET_RESOLVED_FILTER;
 import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
 
 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.PLUGIN_CHANGED;
 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.pluginEnabledKey;
@@ -26,10 +29,8 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -130,7 +131,7 @@
     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
         EditText filterBox = view.findViewById(R.id.filter_box);
-        filterBox.setVisibility(View.VISIBLE);
+        filterBox.setVisibility(VISIBLE);
         filterBox.addTextChangedListener(new TextWatcher() {
             @Override
             public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
@@ -212,7 +213,7 @@
 
         Set<String> pluginActions = manager.getPluginActions();
 
-        ArrayMap<Pair<String, String>, ArrayList<Pair<String, ServiceInfo>>> plugins =
+        ArrayMap<Pair<String, String>, ArrayList<Pair<String, ResolveInfo>>> plugins =
                 new ArrayMap<>();
 
         Set<String> pluginPermissionApps = pm.getPackagesHoldingPermissions(
@@ -224,7 +225,7 @@
         for (String action : pluginActions) {
             String name = toName(action);
             List<ResolveInfo> result = pm.queryIntentServices(
-                    new Intent(action), MATCH_DISABLED_COMPONENTS);
+                    new Intent(action), MATCH_DISABLED_COMPONENTS | GET_RESOLVED_FILTER);
             for (ResolveInfo info : result) {
                 String packageName = info.serviceInfo.packageName;
                 if (!pluginPermissionApps.contains(packageName)) {
@@ -235,7 +236,7 @@
                 if (!plugins.containsKey(key)) {
                     plugins.put(key, new ArrayList<>());
                 }
-                plugins.get(key).add(Pair.create(name, info.serviceInfo));
+                plugins.get(key).add(Pair.create(name, info));
             }
         }
 
@@ -243,11 +244,11 @@
         plugins.forEach((key, si) -> {
             String packageName = key.first;
             List<ComponentName> componentNames = si.stream()
-                    .map(p -> new ComponentName(packageName, p.second.name))
+                    .map(p -> new ComponentName(packageName, p.second.serviceInfo.name))
                     .collect(Collectors.toList());
             if (!componentNames.isEmpty()) {
                 SwitchPreference pref = new PluginPreference(
-                        prefContext, si.get(0).second.applicationInfo, enabler, componentNames);
+                        prefContext, si.get(0).second, enabler, componentNames);
                 pref.setSummary("Plugins: "
                         + si.stream().map(p -> p.first).collect(Collectors.joining(", ")));
                 mPluginsCategory.addPreference(pref);
@@ -341,21 +342,33 @@
     }
 
     private static class PluginPreference extends SwitchPreference {
-        private final boolean mHasSettings;
-        private final PreferenceDataStore mPluginEnabler;
         private final String mPackageName;
+        private final ResolveInfo mSettingsInfo;
+        private final PreferenceDataStore mPluginEnabler;
         private final List<ComponentName> mComponentNames;
 
-        PluginPreference(Context prefContext, ApplicationInfo info,
+        PluginPreference(Context prefContext, ResolveInfo pluginInfo,
                 PreferenceDataStore pluginEnabler, List<ComponentName> componentNames) {
             super(prefContext);
             PackageManager pm = prefContext.getPackageManager();
-            mHasSettings = pm.resolveActivity(new Intent(ACTION_PLUGIN_SETTINGS)
-                    .setPackage(info.packageName), 0) != null;
-            mPackageName = info.packageName;
-            mComponentNames = componentNames;
+            mPackageName = pluginInfo.serviceInfo.applicationInfo.packageName;
+            Intent settingsIntent = new Intent(ACTION_PLUGIN_SETTINGS).setPackage(mPackageName);
+            // If any Settings activity in app has category filters, set plugin action as category.
+            List<ResolveInfo> settingsInfos =
+                    pm.queryIntentActivities(settingsIntent, GET_RESOLVED_FILTER);
+            if (pluginInfo.filter != null) {
+                for (ResolveInfo settingsInfo : settingsInfos) {
+                    if (settingsInfo.filter != null && settingsInfo.filter.countCategories() > 0) {
+                        settingsIntent.addCategory(pluginInfo.filter.getAction(0));
+                        break;
+                    }
+                }
+            }
+
+            mSettingsInfo = pm.resolveActivity(settingsIntent, 0);
             mPluginEnabler = pluginEnabler;
-            setTitle(info.loadLabel(pm));
+            mComponentNames = componentNames;
+            setTitle(pluginInfo.loadLabel(pm));
             setChecked(isPluginEnabled());
             setWidgetLayoutResource(R.layout.switch_preference_with_settings);
         }
@@ -396,17 +409,14 @@
         @Override
         public void onBindViewHolder(PreferenceViewHolder holder) {
             super.onBindViewHolder(holder);
-            holder.findViewById(R.id.settings).setVisibility(mHasSettings ? View.VISIBLE
-                    : View.GONE);
-            holder.findViewById(R.id.divider).setVisibility(mHasSettings ? View.VISIBLE
-                    : View.GONE);
+            boolean hasSettings = mSettingsInfo != null;
+            holder.findViewById(R.id.settings).setVisibility(hasSettings ? VISIBLE : GONE);
+            holder.findViewById(R.id.divider).setVisibility(hasSettings ? VISIBLE : GONE);
             holder.findViewById(R.id.settings).setOnClickListener(v -> {
-                ResolveInfo result = v.getContext().getPackageManager().resolveActivity(
-                        new Intent(ACTION_PLUGIN_SETTINGS).setPackage(mPackageName), 0);
-                if (result != null) {
+                if (hasSettings) {
                     v.getContext().startActivity(new Intent().setComponent(
-                            new ComponentName(result.activityInfo.packageName,
-                                    result.activityInfo.name)));
+                            new ComponentName(mSettingsInfo.activityInfo.packageName,
+                                    mSettingsInfo.activityInfo.name)));
                 }
             });
             holder.itemView.setOnLongClickListener(v -> {
diff --git a/src/com/android/launcher3/settings/NotificationDotsPreference.java b/src/com/android/launcher3/settings/NotificationDotsPreference.java
index a91303a..a354169 100644
--- a/src/com/android/launcher3/settings/NotificationDotsPreference.java
+++ b/src/com/android/launcher3/settings/NotificationDotsPreference.java
@@ -35,14 +35,14 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.notification.NotificationListener;
-import com.android.launcher3.util.SecureSettingsObserver;
+import com.android.launcher3.util.SettingsCache;
 
 /**
  * A {@link Preference} for indicating notification dots status.
  * Also has utility methods for updating UI based on dots status changes.
  */
 public class NotificationDotsPreference extends Preference
-        implements SecureSettingsObserver.OnChangeListener {
+        implements SettingsCache.OnChangeListener {
 
     private boolean mWidgetFrameVisible = false;
 
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index 922425f..f03065c 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -18,13 +18,13 @@
 
 import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
 
+import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
+import static com.android.launcher3.util.SettingsCache.NOTIFICATION_ENABLED_LISTENERS;
 import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
 import static com.android.launcher3.states.RotationHelper.getAllowRotationDefaultValue;
-import static com.android.launcher3.util.SecureSettingsObserver.newNotificationSettingsObserver;
 
 import android.content.SharedPreferences;
 import android.os.Bundle;
-import android.provider.Settings;
 import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
@@ -45,8 +45,8 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.WidgetsModel;
+import com.android.launcher3.util.SettingsCache;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
-import com.android.launcher3.util.SecureSettingsObserver;
 
 /**
  * Settings activity for Launcher. Currently implements the following setting: Allow rotation
@@ -59,8 +59,6 @@
     private static final String FLAGS_PREFERENCE_KEY = "flag_toggler";
 
     private static final String NOTIFICATION_DOTS_PREFERENCE_KEY = "pref_icon_badging";
-    /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */
-    private static final String NOTIFICATION_ENABLED_LISTENERS = "enabled_notification_listeners";
 
     public static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
     public static final String EXTRA_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args";
@@ -126,10 +124,12 @@
      */
     public static class LauncherSettingsFragment extends PreferenceFragmentCompat {
 
-        private SecureSettingsObserver mNotificationDotsObserver;
+        private SettingsCache mSettingsCache;
 
         private String mHighLightKey;
         private boolean mPreferenceHighlighted = false;
+        private NotificationDotsPreference mNotificationSettingsChangedListener;
+        private Preference mDeveloperOptionPref;
 
         @Override
         public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
@@ -177,14 +177,16 @@
                     }
 
                     // Listen to system notification dot settings while this UI is active.
-                    mNotificationDotsObserver = newNotificationSettingsObserver(
-                            getActivity(), (NotificationDotsPreference) preference);
-                    mNotificationDotsObserver.register();
+                    mSettingsCache = SettingsCache.INSTANCE.get(getActivity());
+                    mNotificationSettingsChangedListener =
+                            ((NotificationDotsPreference) preference);
+                    mSettingsCache.register(NOTIFICATION_BADGING_URI,
+                            (NotificationDotsPreference) mNotificationSettingsChangedListener);
                     // Also listen if notification permission changes
-                    mNotificationDotsObserver.getResolver().registerContentObserver(
-                            Settings.Secure.getUriFor(NOTIFICATION_ENABLED_LISTENERS), false,
-                            mNotificationDotsObserver);
-                    mNotificationDotsObserver.dispatchOnChange();
+                    mSettingsCache.register(NOTIFICATION_ENABLED_LISTENERS,
+                            mNotificationSettingsChangedListener);
+                    mSettingsCache.dispatchOnChange(NOTIFICATION_BADGING_URI);
+                    mSettingsCache.dispatchOnChange(NOTIFICATION_ENABLED_LISTENERS);
                     return true;
 
                 case ALLOW_ROTATION_PREFERENCE_KEY:
@@ -201,18 +203,37 @@
                     return FeatureFlags.showFlagTogglerUi(getContext());
 
                 case DEVELOPER_OPTIONS_KEY:
-                    // Show if plugins are enabled or flag UI is enabled.
-                    return FeatureFlags.showFlagTogglerUi(getContext()) ||
-                            PluginManagerWrapper.hasPlugins(getContext());
+                    mDeveloperOptionPref = preference;
+                    return updateDeveloperOption();
             }
 
             return true;
         }
 
+        /**
+         * Show if plugins are enabled or flag UI is enabled.
+         * @return True if we should show the preference option.
+         */
+        private boolean updateDeveloperOption() {
+            boolean showPreference = FeatureFlags.showFlagTogglerUi(getContext())
+                    || PluginManagerWrapper.hasPlugins(getContext());
+            if (mDeveloperOptionPref != null) {
+                mDeveloperOptionPref.setEnabled(showPreference);
+                if (showPreference) {
+                    getPreferenceScreen().addPreference(mDeveloperOptionPref);
+                } else {
+                    getPreferenceScreen().removePreference(mDeveloperOptionPref);
+                }
+            }
+            return showPreference;
+        }
+
         @Override
         public void onResume() {
             super.onResume();
 
+            updateDeveloperOption();
+
             if (isAdded() && !mPreferenceHighlighted) {
                 PreferenceHighlighter highlighter = createHighlighter();
                 if (highlighter != null) {
@@ -251,9 +272,11 @@
 
         @Override
         public void onDestroy() {
-            if (mNotificationDotsObserver != null) {
-                mNotificationDotsObserver.unregister();
-                mNotificationDotsObserver = null;
+            if (mSettingsCache != null) {
+                mSettingsCache.unregister(NOTIFICATION_BADGING_URI,
+                        mNotificationSettingsChangedListener);
+                mSettingsCache.unregister(NOTIFICATION_ENABLED_LISTENERS,
+                        mNotificationSettingsChangedListener);
             }
             super.onDestroy();
         }
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index beb5b68..a18f340 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -302,10 +302,6 @@
     }
 
     private PendingAnimation createAnimationToNewWorkspaceInternal(final STATE_TYPE state) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "createAnimationToNewWorkspaceInternal: "
-                    + state);
-        }
         PendingAnimation builder = new PendingAnimation(mConfig.duration);
         if (mConfig.getAnimComponents() != 0) {
             for (StateHandler handler : getStateHandlers()) {
@@ -328,9 +324,6 @@
 
             @Override
             public void onAnimationSuccess(Animator animator) {
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "onAnimationSuccess: " + state);
-                }
                 onStateTransitionEnd(state);
             }
         };
diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
index 601e117..7abb653 100644
--- a/src/com/android/launcher3/statemanager/StatefulActivity.java
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -150,8 +150,8 @@
 
     private void handleDeferredResume() {
         if (hasBeenResumed() && !getStateManager().getState().hasFlag(FLAG_NON_INTERACTIVE)) {
-            onDeferredResumed();
             addActivityFlags(ACTIVITY_STATE_DEFERRED_RESUMED);
+            onDeferredResumed();
 
             mDeferredResumePending = false;
         } else {
diff --git a/src/com/android/launcher3/states/SpringLoadedState.java b/src/com/android/launcher3/states/SpringLoadedState.java
index 45172b5..fce8fff 100644
--- a/src/com/android/launcher3/states/SpringLoadedState.java
+++ b/src/com/android/launcher3/states/SpringLoadedState.java
@@ -90,4 +90,9 @@
     public float getWorkspaceScrimAlpha(Launcher launcher) {
         return 0.3f;
     }
+
+    @Override
+    public int getVisibleElements(Launcher launcher) {
+        return (super.getVisibleElements(launcher) | HOTSEAT_ICONS) & ~TASKBAR;
+    }
 }
diff --git a/src/com/android/launcher3/states/StateAnimationConfig.java b/src/com/android/launcher3/states/StateAnimationConfig.java
index 8b72177..e4c67ee 100644
--- a/src/com/android/launcher3/states/StateAnimationConfig.java
+++ b/src/com/android/launcher3/states/StateAnimationConfig.java
@@ -37,7 +37,8 @@
             PLAY_ATOMIC_OVERVIEW_SCALE,
             PLAY_ATOMIC_OVERVIEW_PEEK,
             SKIP_OVERVIEW,
-            SKIP_DEPTH_CONTROLLER
+            SKIP_DEPTH_CONTROLLER,
+            SKIP_TASKBAR,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AnimationFlags {}
@@ -46,6 +47,7 @@
     public static final int PLAY_ATOMIC_OVERVIEW_PEEK = 1 << 2;
     public static final int SKIP_OVERVIEW = 1 << 3;
     public static final int SKIP_DEPTH_CONTROLLER = 1 << 4;
+    public static final int SKIP_TASKBAR = 1 << 5;
 
     public long duration;
     public boolean userControlled;
@@ -72,6 +74,8 @@
             ANIM_OVERVIEW_MODAL,
             ANIM_DEPTH,
             ANIM_OVERVIEW_ACTIONS_FADE,
+            ANIM_TASKBAR_FADE,
+            ANIM_HOTSEAT_FADE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AnimType {}
@@ -91,8 +95,10 @@
     public static final int ANIM_OVERVIEW_MODAL = 13;
     public static final int ANIM_DEPTH = 14;
     public static final int ANIM_OVERVIEW_ACTIONS_FADE = 15;
+    public static final int ANIM_TASKBAR_FADE = 16;
+    public static final int ANIM_HOTSEAT_FADE = 17; // if not set, falls back to ANIM_WORKSPACE_FADE
 
-    private static final int ANIM_TYPES_COUNT = 16;
+    private static final int ANIM_TYPES_COUNT = 18;
 
     protected final Interpolator[] mInterpolators = new Interpolator[ANIM_TYPES_COUNT];
 
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index d4a132e..4261d08 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -33,7 +33,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.util.ResourceBasedOverride;
-import com.android.launcher3.widget.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
 
 import java.util.concurrent.ExecutionException;
 import java.util.function.Function;
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 218172b..7cb6e34 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -103,7 +103,8 @@
     public static final String REQUEST_MOCK_SENSOR_ROTATION = "mock-sensor-rotation";
 
     public static final String PERMANENT_DIAG_TAG = "TaplTarget";
-    public static final String OVERIEW_NOT_ALLAPPS = "b/156095088";
     public static final String NO_SWIPE_TO_HOME = "b/158017601";
     public static final String WORK_PROFILE_REMOVED = "b/159671700";
+    public static final String TIS_NO_EVENTS = "b/180915942";
+    public static final String GET_RECENTS_FAILED = "b/177472267";
 }
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 10cd04c..6f1b2f9 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -67,8 +67,9 @@
 
     /**
      * Play an atomic recents animation when the progress from NORMAL to OVERVIEW reaches this.
+     * TODO: Remove the atomic animation altogether and just go to OVERVIEW directly (b/175137718).
      */
-    public static final float ATOMIC_OVERVIEW_ANIM_THRESHOLD = 0.5f;
+    public static final float ATOMIC_OVERVIEW_ANIM_THRESHOLD = 1f;
     protected final long ATOMIC_DURATION = getAtomicDuration();
 
     protected final Launcher mLauncher;
@@ -124,7 +125,7 @@
     protected abstract boolean canInterceptTouch(MotionEvent ev);
 
     @Override
-    public final boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             mNoIntercept = !canInterceptTouch(ev);
             if (mNoIntercept) {
@@ -193,16 +194,14 @@
                 : reachedToState ? mToState : mFromState;
         LauncherState newToState = getTargetState(newFromState, isDragTowardPositive);
 
+        onReinitToState(newToState);
+
         if (newFromState == mFromState && newToState == mToState || (newFromState == newToState)) {
             return false;
         }
 
         mFromState = newFromState;
         mToState = newToState;
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "reinitCurrentAnimation: "
-                    + newToState.ordinal + " " + getClass().getSimpleName());
-        }
 
         mStartProgress = 0;
         mPassedOverviewAtomicThreshold = false;
@@ -231,6 +230,12 @@
         return true;
     }
 
+    protected void onReinitToState(LauncherState newToState) {
+    }
+
+    protected void onReachedFinalState(LauncherState newToState) {
+    }
+
     protected boolean goingBetweenNormalAndOverview(LauncherState fromState,
             LauncherState toState) {
         return (fromState == NORMAL || fromState == OVERVIEW)
@@ -262,7 +267,6 @@
         mFlingBlockCheck.unblockFling();
         // Must be called after all the animation controllers have been paused
         if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()
-                && !FeatureFlags.DISABLE_INITIAL_IME_IN_ALLAPPS.get()
                 && BuildCompat.isAtLeastR()
                 && (mToState == ALL_APPS || mToState == NORMAL)) {
             mLauncher.getAllAppsController().getInsetController().onDragStart(
@@ -335,9 +339,7 @@
         if (!goingBetweenNormalAndOverview(fromState, toState)) {
             return;
         }
-        float threshold = toState == OVERVIEW ? ATOMIC_OVERVIEW_ANIM_THRESHOLD
-                : 1f - ATOMIC_OVERVIEW_ANIM_THRESHOLD;
-        boolean passedThreshold = progress >= threshold;
+        boolean passedThreshold = progress >= ATOMIC_OVERVIEW_ANIM_THRESHOLD;
         if (passedThreshold != mPassedOverviewAtomicThreshold) {
             LauncherState atomicFromState = passedThreshold ? fromState: toState;
             LauncherState atomicToState = passedThreshold ? toState : fromState;
@@ -527,6 +529,7 @@
             mAtomicComponentsController.getAnimationPlayer().end();
             mAtomicComponentsController = null;
         }
+        onReachedFinalState(mToState);
         clearState();
         boolean shouldGoToTargetState = mGoingBetweenStates || (mToState != targetState);
         if (shouldGoToTargetState) {
@@ -543,7 +546,8 @@
             // case the user started interacting with it before the animation finished.
             mLauncher.getStateManager().goToState(targetState, false /* animated */);
         }
-        mLauncher.getDragLayer().getScrim().createSysuiMultiplierAnim(1f).setDuration(0).start();
+        mLauncher.getDragLayer().getSysUiScrim().createSysuiMultiplierAnim(
+                1f).setDuration(0).start();
     }
 
     private void logReachedState(LauncherState targetState) {
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index 3122c68..61bd30a 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.Launcher.REQUEST_BIND_PENDING_APPWIDGET;
 import static com.android.launcher3.Launcher.REQUEST_RECONFIGURE_APPWIDGET;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_BY_PUBLISHER;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
@@ -40,7 +41,6 @@
 
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.folder.Folder;
@@ -58,6 +58,7 @@
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.views.FloatingIconView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.PendingAppWidgetHostView;
 import com.android.launcher3.widget.WidgetAddFlowHandler;
 import com.android.launcher3.widget.WidgetManagerHelper;
@@ -272,6 +273,7 @@
                         Toast.LENGTH_SHORT).show();
             }
         }
+        launcher.getStatsLogManager().logger().withItemInfo(itemInfo).log(LAUNCHER_APP_LAUNCH_TAP);
     }
 
     private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher) {
@@ -302,7 +304,7 @@
                 intent.setPackage(null);
             }
         }
-        if (v != null && launcher.getAppTransitionManager().supportsAdaptiveIconAnimation()) {
+        if (v != null && launcher.getAppTransitionManager().supportsAdaptiveIconAnimation(v)) {
             // Preload the icon to reduce latency b/w swapping the floating view with the original.
             FloatingIconView.fetchIcon(launcher, v, item, true /* isOpening */);
         }
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index 7baeab8..f876dd9 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_ITEM_LONG_PRESSED;
 
 import android.view.View;
 import android.view.View.OnLongClickListener;
@@ -32,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.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
@@ -55,7 +57,7 @@
         if (!(v.getTag() instanceof ItemInfo)) return false;
 
         launcher.setWaitingForResult(null);
-        beginDrag(v, launcher, (ItemInfo) v.getTag(), new DragOptions());
+        beginDrag(v, launcher, (ItemInfo) v.getTag(), launcher.getDefaultWorkspaceDragOptions());
         return true;
     }
 
@@ -86,6 +88,12 @@
         if (!launcher.isInState(ALL_APPS) && !launcher.isInState(OVERVIEW)) return false;
         if (launcher.getWorkspace().isSwitchingState()) return false;
 
+        StatsLogger logger = launcher.getStatsLogManager().logger();
+        if (v.getTag() instanceof ItemInfo) {
+            logger.withItemInfo((ItemInfo) v.getTag());
+        }
+        logger.log(LAUNCHER_ALLAPPS_ITEM_LONG_PRESSED);
+
         // Start the drag
         final DragController dragController = launcher.getDragController();
         dragController.addDragListener(new DragController.DragListener() {
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index f05f15e..8a64f3d 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -147,13 +147,6 @@
     }
 
     @Override
-    public void setPrimaryAndResetSecondaryTranslate(
-            View view, float translation, float defaultTranslationX, float defaultTranslationY) {
-        view.setTranslationX(defaultTranslationX);
-        view.setTranslationY(translation);
-    }
-
-    @Override
     public int getPrimaryScroll(View view) {
         return view.getScrollY();
     }
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index b9acfa3..e1cec87 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -66,8 +66,6 @@
     int getSecondaryDimension(View view);
     FloatProperty<View> getPrimaryViewTranslate();
     FloatProperty<View> getSecondaryViewTranslate();
-    void setPrimaryAndResetSecondaryTranslate(
-            View view, float translation, float defaultTranslationX, float defaultTranslationY);
     int getPrimaryScroll(View view);
     float getPrimaryScale(View view);
     int getChildStart(View view);
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index 3663b5f..bcaf5f4 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -143,13 +143,6 @@
     }
 
     @Override
-    public void setPrimaryAndResetSecondaryTranslate(
-            View view, float translation, float defaultTranslationX, float defaultTranslationY) {
-        view.setTranslationX(translation);
-        view.setTranslationY(defaultTranslationY);
-    }
-
-    @Override
     public int getPrimaryScroll(View view) {
         return view.getScrollX();
     }
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 3b7bcc2..d0e8bb1 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -29,6 +29,7 @@
 import android.util.SparseArray;
 import android.view.Display;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Utilities;
@@ -182,20 +183,30 @@
         }
 
         /** Creates and up-to-date DisplayController.Info for the given context. */
+        @Nullable
         public Info createInfoForContext(Context context) {
-            Display display = Utilities.ATLEAST_R
-                    ? context.getDisplay()
-                    : context
-                        .getSystemService(DisplayManager.class)
-                        .getDisplay(mId);
-            return display == null
-                    ? new Info(context)
-                    : new Info(context, display);
+            Display display = Utilities.ATLEAST_R ? context.getDisplay() : null;
+            if (display == null) {
+                display = context.getSystemService(DisplayManager.class).getDisplay(mId);
+            }
+            if (display == null) {
+                return null;
+            }
+            // Refresh the Context the prevent stale DisplayMetrics.
+            Context displayContext = context.getApplicationContext().createDisplayContext(display);
+            return new Info(displayContext, display);
+        }
+
+        public Context getDisplayContext() {
+            return mDisplayContext;
         }
 
         protected void handleOnChange() {
             Info oldInfo = mInfo;
             Info newInfo = createInfoForContext(mDisplayContext);
+            if (newInfo == null) {
+                return;
+            }
 
             int change = 0;
             if (newInfo.hasDifferentSize(oldInfo)) {
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 7b26427..08ec591 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -92,6 +92,14 @@
     }
 
     /**
+     * Returns whether the target app is installed for a given user
+     */
+    public boolean isAppInstalled(String packageName, UserHandle user) {
+        ApplicationInfo info = getApplicationInfo(packageName, user, 0);
+        return info != null;
+    }
+
+    /**
      * Returns the application info for the provided package or null
      */
     public ApplicationInfo getApplicationInfo(String packageName, UserHandle user, int flags) {
@@ -105,7 +113,7 @@
     }
 
     public boolean isSafeMode() {
-        return mContext.getPackageManager().isSafeMode();
+        return mPm.isSafeMode();
     }
 
     public Intent getAppLaunchIntent(String pkg, UserHandle user) {
diff --git a/src/com/android/launcher3/util/SecureSettingsObserver.java b/src/com/android/launcher3/util/SecureSettingsObserver.java
deleted file mode 100644
index 9fe72ad..0000000
--- a/src/com/android/launcher3/util/SecureSettingsObserver.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.util;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.os.Handler;
-import android.provider.Settings;
-
-/**
- * Utility class to listen for secure settings changes
- */
-public class SecureSettingsObserver extends ContentObserver {
-
-    /** Hidden field Settings.Secure.NOTIFICATION_BADGING */
-    public static final String NOTIFICATION_BADGING = "notification_badging";
-    /** Hidden field Settings.Secure.ONE_HANDED_MODE_ENABLED */
-    public static final String ONE_HANDED_ENABLED = "one_handed_mode_enabled";
-    /** Hidden field Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED */
-    public static final String ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED =
-            "swipe_bottom_to_notification_enabled";
-
-    private final ContentResolver mResolver;
-    private final String mKeySetting;
-    private final int mDefaultValue;
-    private final OnChangeListener mOnChangeListener;
-
-    public SecureSettingsObserver(ContentResolver resolver, OnChangeListener listener,
-            String keySetting, int defaultValue) {
-        super(new Handler());
-
-        mResolver = resolver;
-        mOnChangeListener = listener;
-        mKeySetting = keySetting;
-        mDefaultValue = defaultValue;
-    }
-
-    @Override
-    public void onChange(boolean selfChange) {
-        mOnChangeListener.onSettingsChanged(getValue());
-    }
-
-    public boolean getValue() {
-        return Settings.Secure.getInt(mResolver, mKeySetting, mDefaultValue) == 1;
-    }
-
-    public void register() {
-        mResolver.registerContentObserver(Settings.Secure.getUriFor(mKeySetting), false, this);
-    }
-
-    public ContentResolver getResolver() {
-        return mResolver;
-    }
-
-    public void dispatchOnChange() {
-        onChange(true);
-    }
-
-    public void unregister() {
-        mResolver.unregisterContentObserver(this);
-    }
-
-    public interface OnChangeListener {
-        void onSettingsChanged(boolean isEnabled);
-    }
-
-    public static SecureSettingsObserver newNotificationSettingsObserver(Context context,
-            OnChangeListener listener) {
-        return new SecureSettingsObserver(
-                context.getContentResolver(), listener, NOTIFICATION_BADGING, 1);
-    }
-
-    public static SecureSettingsObserver newOneHandedSettingsObserver(Context context,
-            OnChangeListener listener) {
-        return new SecureSettingsObserver(
-                context.getContentResolver(), listener, ONE_HANDED_ENABLED, 0);
-    }
-
-    /**
-     * Constructs settings observer for {@link #ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED}
-     * preference.
-     */
-    public static SecureSettingsObserver newSwipeToNotificationSettingsObserver(Context context,
-            OnChangeListener listener) {
-        return new SecureSettingsObserver(
-                context.getContentResolver(), listener,
-                ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED, 1);
-    }
-}
diff --git a/src/com/android/launcher3/util/SettingsCache.java b/src/com/android/launcher3/util/SettingsCache.java
new file mode 100644
index 0000000..22b4d38
--- /dev/null
+++ b/src/com/android/launcher3/util/SettingsCache.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2021 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 android.provider.Settings.System.ACCELEROMETER_ROTATION;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * ContentObserver over Settings keys that also has a caching layer.
+ * Consumers can register for callbacks via {@link #register(Uri, OnChangeListener)} and
+ * {@link #unregister(Uri, OnChangeListener)} methods.
+ *
+ * This can be used as a normal cache without any listeners as well via the
+ * {@link #getValue(Uri, int)} and {@link #dispatchOnChange(Uri)} to update (and subsequently call
+ * get)
+ *
+ * The cache will be invalidated/updated through the normal
+ * {@link ContentObserver#onChange(boolean)} calls
+ * or can be force updated by calling {@link #dispatchOnChange(Uri)}.
+ *
+ * Cache will also be updated if a key queried is missing (even if it has no listeners registered).
+ */
+public class SettingsCache extends ContentObserver {
+
+    /** Hidden field Settings.Secure.NOTIFICATION_BADGING */
+    public static final Uri NOTIFICATION_BADGING_URI =
+            Settings.Secure.getUriFor("notification_badging");
+    /** Hidden field Settings.Secure.ONE_HANDED_MODE_ENABLED */
+    public static final String ONE_HANDED_ENABLED = "one_handed_mode_enabled";
+    /** Hidden field Settings.Secure.SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED */
+    public static final String ONE_HANDED_SWIPE_BOTTOM_TO_NOTIFICATION_ENABLED =
+            "swipe_bottom_to_notification_enabled";
+    /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */
+    public static final Uri NOTIFICATION_ENABLED_LISTENERS =
+            Settings.Secure.getUriFor("enabled_notification_listeners");
+    public static final Uri ROTATION_SETTING_URI =
+            Settings.System.getUriFor(ACCELEROMETER_ROTATION);
+
+    private static final String SYSTEM_URI_PREFIX = Settings.System.CONTENT_URI.toString();
+
+    /**
+     * Caches the last seen value for registered keys.
+     */
+    private Map<Uri, Boolean> mKeyCache = new ConcurrentHashMap<>();
+    private final Map<Uri, CopyOnWriteArrayList<OnChangeListener>> mListenerMap = new HashMap<>();
+    protected final ContentResolver mResolver;
+
+
+    /**
+     * Singleton instance
+     */
+    public static MainThreadInitializedObject<SettingsCache> INSTANCE =
+            new MainThreadInitializedObject<>(SettingsCache::new);
+
+    private SettingsCache(Context context) {
+        super(new Handler());
+        mResolver = context.getContentResolver();
+    }
+
+    @Override
+    public void onChange(boolean selfChange, Uri uri) {
+        // We use default of 1, but if we're getting an onChange call, can assume a non-default
+        // value will exist
+        boolean newVal = updateValue(uri, 1 /* Effectively Unused */);
+        if (!mListenerMap.containsKey(uri)) {
+            return;
+        }
+
+        for (OnChangeListener listener : mListenerMap.get(uri)) {
+            listener.onSettingsChanged(newVal);
+        }
+    }
+
+    /**
+     * Returns the value for this classes key from the cache. If not in cache, will call
+     * {@link #updateValue(Uri, int)} to fetch.
+     */
+    public boolean getValue(Uri keySetting, int defaultValue) {
+        if (mKeyCache.containsKey(keySetting)) {
+            return mKeyCache.get(keySetting);
+        } else {
+            return updateValue(keySetting, defaultValue);
+        }
+    }
+
+    /**
+     * Does not de-dupe if you add same listeners for the same key multiple times.
+     * Unregister once complete using {@link #unregister(Uri, OnChangeListener)}
+     */
+    public void register(Uri uri, OnChangeListener changeListener) {
+        if (mListenerMap.containsKey(uri)) {
+            mListenerMap.get(uri).add(changeListener);
+        } else {
+            CopyOnWriteArrayList<OnChangeListener> l = new CopyOnWriteArrayList<>();
+            l.add(changeListener);
+            mListenerMap.put(uri, l);
+            mResolver.registerContentObserver(uri, false, this);
+        }
+    }
+
+    private boolean updateValue(Uri keyUri, int defaultValue) {
+        String key = keyUri.getLastPathSegment();
+        boolean newVal;
+        if (keyUri.toString().startsWith(SYSTEM_URI_PREFIX)) {
+            newVal = Settings.System.getInt(mResolver, key, defaultValue) == 1;
+        } else { // SETTING_SECURE
+            newVal = Settings.Secure.getInt(mResolver, key, defaultValue) == 1;
+        }
+
+        mKeyCache.put(keyUri, newVal);
+        return newVal;
+    }
+
+    /**
+     * Force update a change for a given URI and have all listeners for that URI receive callbacks
+     * even if the value is unchanged.
+     */
+    public void dispatchOnChange(Uri uri) {
+        onChange(true, uri);
+    }
+
+    /**
+     * Call to stop receiving updates on the given {@param listener}.
+     * This Uri/Listener pair must correspond to the same pair called with for
+     * {@link #register(Uri, OnChangeListener)}
+     */
+    public void unregister(Uri uri, OnChangeListener listener) {
+        List<OnChangeListener> listenersToRemoveFrom = mListenerMap.get(uri);
+        if (!listenersToRemoveFrom.contains(listener)) {
+            return;
+        }
+
+        listenersToRemoveFrom.remove(listener);
+        if (listenersToRemoveFrom.isEmpty()) {
+            mListenerMap.remove(uri);
+        }
+    }
+
+    /**
+     * Don't use this. Ever.
+     * @param keyCache Cache to replace {@link #mKeyCache}
+     */
+    @VisibleForTesting
+    void setKeyCache(Map<Uri, Boolean> keyCache) {
+        mKeyCache = keyCache;
+    }
+
+    public interface OnChangeListener {
+        void onSettingsChanged(boolean isEnabled);
+    }
+}
diff --git a/src/com/android/launcher3/util/Themes.java b/src/com/android/launcher3/util/Themes.java
index b74686f..512a286 100644
--- a/src/com/android/launcher3/util/Themes.java
+++ b/src/com/android/launcher3/util/Themes.java
@@ -81,6 +81,16 @@
         return getAttrColor(context, android.R.attr.colorAccent);
     }
 
+    /** Returns the floating background color attribute. */
+    public static int getColorBackground(Context context) {
+        return getAttrColor(context, android.R.attr.colorBackground);
+    }
+
+    /** Returns the floating background color attribute. */
+    public static int getColorBackgroundFloating(Context context) {
+        return getAttrColor(context, android.R.attr.colorBackgroundFloating);
+    }
+
     public static int getAttrColor(Context context, int attr) {
         TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
         int colorAccent = ta.getColor(0, 0);
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index ae459e1..505c6ce 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -17,7 +17,7 @@
 
 import android.content.Context;
 import android.content.ContextWrapper;
-import android.view.ContextThemeWrapper;
+import android.graphics.Rect;
 import android.view.View.AccessibilityDelegate;
 
 import com.android.launcher3.DeviceProfile;
@@ -49,6 +49,23 @@
         return null;
     }
 
+    default Rect getFolderBoundingBox() {
+        return getDeviceProfile().getAbsoluteOpenFolderBounds();
+    }
+
+    /**
+     * After calling {@link #getFolderBoundingBox()}, we calculate a (left, top) position for a
+     * Folder of size width x height to be within those bounds. However, the chosen position may
+     * not be visually ideal (e.g. uncanny valley of centeredness), so here's a chance to update it.
+     * @param inOutPosition A 2-size array where the first element is the left position of the open
+     *     folder and the second element is the top position. Should be updated in place if desired.
+     * @param bounds The bounds that the open folder should fit inside.
+     * @param width The width of the open folder.
+     * @param height The height of the open folder.
+     */
+    default void updateOpenFolderPosition(int[] inOutPosition, Rect bounds, int width, int height) {
+    }
+
     /**
      * The root view to support drag-and-drop and popup support.
      */
@@ -56,10 +73,10 @@
 
     DeviceProfile getDeviceProfile();
 
-    static ActivityContext lookupContext(Context context) {
+    static <T extends ActivityContext> T lookupContext(Context context) {
         if (context instanceof ActivityContext) {
-            return (ActivityContext) context;
-        } else if (context instanceof ContextThemeWrapper) {
+            return (T) context;
+        } else if (context instanceof ContextWrapper) {
             return lookupContext(((ContextWrapper) context).getBaseContext());
         } else {
             throw new IllegalArgumentException("Cannot find ActivityContext in parent tree");
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index 5464dd8..1939d15 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -206,15 +206,19 @@
 
     protected boolean findActiveController(MotionEvent ev) {
         mActiveController = null;
-        if ((mTouchDispatchState & (TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION
-                | TOUCH_DISPATCHING_FROM_PROXY)) == 0) {
-            // Only look for controllers if we are not dispatching from gesture area and proxy is
-            // not active
+        if (canFindActiveController()) {
             mActiveController = findControllerToHandleTouch(ev);
         }
         return mActiveController != null;
     }
 
+    protected boolean canFindActiveController() {
+        // Only look for controllers if we are not dispatching from gesture area and proxy is
+        // not active
+        return (mTouchDispatchState & (TOUCH_DISPATCHING_FROM_VIEW_GESTURE_REGION
+                | TOUCH_DISPATCHING_FROM_PROXY)) == 0;
+    }
+
     @Override
     public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
         // Shortcuts can appear above folder
@@ -455,12 +459,6 @@
     }
 
     @Override
-    public boolean dispatchUnhandledMove(View focused, int direction) {
-        // Consume the unhandled move if a container is open, to avoid switching pages underneath.
-        return AbstractFloatingView.getTopOpenView(mActivity) != null;
-    }
-
-    @Override
     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
         View topView = AbstractFloatingView.getTopOpenView(mActivity);
         if (topView != null) {
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 80f0981..899dcf7 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -49,7 +49,7 @@
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.widget.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -118,7 +118,8 @@
         mTargetRect.roundOut(outPos);
     }
 
-    public static void show(Launcher launcher, RectF targetRect, List<OptionItem> items) {
+    public static OptionsPopupView show(
+            Launcher launcher, RectF targetRect, List<OptionItem> items) {
         OptionsPopupView popup = (OptionsPopupView) launcher.getLayoutInflater()
                 .inflate(R.layout.longpress_options_menu, launcher.getDragLayer(), false);
         popup.mTargetRect = targetRect;
@@ -134,6 +135,7 @@
             popup.mItemMap.put(view, item);
         }
         popup.show();
+        return popup;
     }
 
     @VisibleForTesting
@@ -156,6 +158,14 @@
      */
     public static ArrayList<OptionItem> getOptions(Launcher launcher) {
         ArrayList<OptionItem> options = new ArrayList<>();
+        options.add(new OptionItem(R.string.settings_button_text, R.drawable.ic_setting,
+                LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS,
+                OptionsPopupView::startSettings));
+        if (!WidgetsModel.GO_DISABLE_WIDGETS) {
+            options.add(new OptionItem(R.string.widget_button_text, R.drawable.ic_widget,
+                    LAUNCHER_WIDGETSTRAY_BUTTON_TAP_OR_LONGPRESS,
+                    OptionsPopupView::onWidgetsClicked));
+        }
         int resString = Utilities.existsStyleWallpapers(launcher) ?
                 R.string.styles_wallpaper_button_text : R.string.wallpaper_button_text;
         int resDrawable = Utilities.existsStyleWallpapers(launcher) ?
@@ -163,15 +173,6 @@
         options.add(new OptionItem(resString, resDrawable,
                 IGNORE,
                 OptionsPopupView::startWallpaperPicker));
-        if (!WidgetsModel.GO_DISABLE_WIDGETS) {
-            options.add(new OptionItem(R.string.widget_button_text, R.drawable.ic_widget,
-                    LAUNCHER_WIDGETSTRAY_BUTTON_TAP_OR_LONGPRESS,
-                    OptionsPopupView::onWidgetsClicked));
-        }
-        options.add(new OptionItem(R.string.settings_button_text, R.drawable.ic_setting,
-                LAUNCHER_SETTINGS_BUTTON_TAP_OR_LONGPRESS,
-                OptionsPopupView::startSettings));
-
         return options;
     }
 
diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
index 804fb3e..ae34257 100644
--- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java
+++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
@@ -34,6 +34,7 @@
 import android.view.ViewConfiguration;
 import android.widget.TextView;
 
+import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.BaseRecyclerView;
@@ -99,6 +100,7 @@
     private boolean mIsThumbDetached;
     private final boolean mCanThumbDetach;
     private boolean mIgnoreDragGesture;
+    private boolean mIsRecyclerViewFirstChildInParent = true;
 
     // This is the offset from the top of the scrollbar when the user first starts touching.  To
     // prevent jumping, this offset is applied as the user scrolls.
@@ -112,6 +114,7 @@
 
     protected BaseRecyclerView mRv;
     private RecyclerView.OnScrollListener mOnScrollListener;
+    @Nullable private OnFastScrollChangeListener mOnFastScrollChangeListener;
 
     private int mDownX;
     private int mDownY;
@@ -188,6 +191,9 @@
         updatePopupY(y);
         mThumbOffsetY = y;
         invalidate();
+        if (mOnFastScrollChangeListener != null) {
+            mOnFastScrollChangeListener.onThumbOffsetYChanged(mThumbOffsetY);
+        }
     }
 
     public int getThumbOffsetY() {
@@ -391,7 +397,9 @@
             return false;
         }
         getHitRect(sTempRect);
-        sTempRect.top += mRv.getScrollBarTop();
+        if (mIsRecyclerViewFirstChildInParent) {
+            sTempRect.top += mRv.getScrollBarTop();
+        }
         if (outOffset != null) {
             outOffset.set(sTempRect.left, sTempRect.top);
         }
@@ -404,4 +412,23 @@
         // alpha is so low, it does not matter.
         return false;
     }
+
+    public void setIsRecyclerViewFirstChildInParent(boolean isRecyclerViewFirstChildInParent) {
+        mIsRecyclerViewFirstChildInParent = isRecyclerViewFirstChildInParent;
+    }
+
+    public void setOnFastScrollChangeListener(
+            @Nullable OnFastScrollChangeListener onFastScrollChangeListener) {
+        mOnFastScrollChangeListener = onFastScrollChangeListener;
+    }
+
+    /**
+     * A callback that is invoked when there is a scroll change in {@link RecyclerViewFastScroller}.
+     */
+    public interface OnFastScrollChangeListener {
+        /**
+         * Called when the thumb offset vertical position, in pixels, has changed to {@code y}.
+         */
+        void onThumbOffsetYChanged(int y);
+    }
 }
diff --git a/src/com/android/launcher3/views/ScrimView.java b/src/com/android/launcher3/views/ScrimView.java
index 77cec80..c9bd284 100644
--- a/src/com/android/launcher3/views/ScrimView.java
+++ b/src/com/android/launcher3/views/ScrimView.java
@@ -42,7 +42,7 @@
  */
 public class ScrimView<T extends Launcher> extends View implements Insettable, OnChangeListener {
 
-    private static final float SCRIM_ALPHA = .75f;
+    private static final float SCRIM_ALPHA = .95f;
     protected final T mLauncher;
     private final WallpaperColorInfo mWallpaperColorInfo;
     protected final int mEndScrim;
@@ -63,6 +63,7 @@
         mWallpaperColorInfo = WallpaperColorInfo.INSTANCE.get(context);
         int endScrim = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
         if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
+            endScrim = Themes.getColorBackgroundFloating(context);
             endScrim = ColorUtils.setAlphaComponent(endScrim, (int) (255  * SCRIM_ALPHA));
         }
         mEndScrim = endScrim;
diff --git a/src/com/android/launcher3/views/WorkEduView.java b/src/com/android/launcher3/views/WorkEduView.java
index 03d3026..c8cf627 100644
--- a/src/com/android/launcher3/views/WorkEduView.java
+++ b/src/com/android/launcher3/views/WorkEduView.java
@@ -168,8 +168,8 @@
     }
 
     private AllAppsPagedView getAllAppsPagedView() {
-        View v =  mLauncher.getAppsView().getContentView();
-        return  (v instanceof AllAppsPagedView)  ? (AllAppsPagedView) v : null;
+        View v = mLauncher.getAppsView().getContentView();
+        return (v instanceof AllAppsPagedView) ? (AllAppsPagedView) v : null;
     }
 
     /**
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index a38e90d..03c58bb 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -85,6 +85,8 @@
 
         if (v instanceof WidgetCell) {
             return beginDraggingWidget((WidgetCell) v);
+        } else if (v.getParent() instanceof WidgetCell) {
+            return beginDraggingWidget((WidgetCell) v.getParent());
         }
         return true;
     }
@@ -99,12 +101,14 @@
             return false;
         }
 
+        PendingItemDragHelper dragHelper = new PendingItemDragHelper(v);
+        dragHelper.setPreview(v.getPreview());
+
         int[] loc = new int[2];
         getPopupContainer().getLocationInDragLayer(image, loc);
 
-        new PendingItemDragHelper(v).startDrag(
-                image.getBitmapBounds(), image.getBitmap().getWidth(), image.getWidth(),
-                new Point(loc[0], loc[1]), this, new DragOptions());
+        dragHelper.startDrag(image.getBitmapBounds(), image.getBitmap().getWidth(),
+                image.getWidth(), new Point(loc[0], loc[1]), this, new DragOptions());
         close(true);
         return true;
     }
diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
similarity index 82%
rename from src/com/android/launcher3/LauncherAppWidgetHost.java
rename to src/com/android/launcher3/widget/LauncherAppWidgetHost.java
index 7ea6851..d745754 100644
--- a/src/com/android/launcher3/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.widget;
 
 import static android.app.Activity.RESULT_CANCELED;
 
@@ -29,12 +29,13 @@
 import android.util.SparseArray;
 import android.widget.Toast;
 
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.widget.DeferredAppWidgetHostView;
-import com.android.launcher3.widget.LauncherAppWidgetHostView;
-import com.android.launcher3.widget.PendingAppWidgetHostView;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.util.ArrayList;
@@ -49,8 +50,11 @@
 public class LauncherAppWidgetHost extends AppWidgetHost {
 
     private static final int FLAG_LISTENING = 1;
-    private static final int FLAG_RESUMED = 1 << 1;
-    private static final int FLAG_LISTEN_IF_RESUMED = 1 << 2;
+    private static final int FLAG_STATE_IS_NORMAL = 1 << 1;
+    private static final int FLAG_ACTIVITY_STARTED = 1 << 2;
+    private static final int FLAG_ACTIVITY_RESUMED = 1 << 3;
+    private static final int FLAGS_SHOULD_LISTEN =
+            FLAG_STATE_IS_NORMAL | FLAG_ACTIVITY_STARTED | FLAG_ACTIVITY_RESUMED;
 
     public static final int APPWIDGET_HOST_ID = 1024;
 
@@ -59,7 +63,7 @@
     private final SparseArray<PendingAppWidgetHostView> mPendingViews = new SparseArray<>();
 
     private final Context mContext;
-    private int mFlags = FLAG_RESUMED;
+    private int mFlags = FLAG_STATE_IS_NORMAL;
 
     private IntConsumer mAppWidgetRemovedCallback = null;
 
@@ -130,49 +134,45 @@
     }
 
     /**
-     * Updates the resumed state of the host.
-     * When a host is not resumed, it defers calls to startListening until host is resumed again.
-     * But if the host was already listening, it will not call stopListening.
-     *
-     * @see #setListenIfResumed(boolean)
+     * Sets or unsets a flag the can change whether the widget host should be in the listening
+     * state.
      */
-    public void setResumed(boolean isResumed) {
-        if (isResumed == ((mFlags & FLAG_RESUMED) != 0)) {
-            return;
-        }
-        if (isResumed) {
-            mFlags |= FLAG_RESUMED;
-            // Start listening if we were supposed to start listening on resume
-            if ((mFlags & FLAG_LISTEN_IF_RESUMED) != 0 && (mFlags & FLAG_LISTENING) == 0) {
-                startListening();
-            }
+    private void setShouldListenFlag(int flag, boolean on) {
+        if (on) {
+            mFlags |= flag;
         } else {
-            mFlags &= ~FLAG_RESUMED;
+            mFlags &= ~flag;
+        }
+
+        final boolean listening = isListening();
+        if (!listening && (mFlags & FLAGS_SHOULD_LISTEN) == FLAGS_SHOULD_LISTEN) {
+            // Postpone starting listening until all flags are on.
+            startListening();
+        } else if (listening && (mFlags & FLAG_ACTIVITY_STARTED) == 0) {
+            // Postpone stopping listening until the activity is stopped.
+            stopListening();
         }
     }
 
     /**
-     * Updates the listening state of the host. If the host is not resumed, startListening is
-     * deferred until next resume.
-     *
-     * @see #setResumed(boolean)
+     * Registers an "entering/leaving Normal state" event.
      */
-    public void setListenIfResumed(boolean listenIfResumed) {
-        if (listenIfResumed == ((mFlags & FLAG_LISTEN_IF_RESUMED) != 0)) {
-            return;
-        }
-        if (listenIfResumed) {
-            mFlags |= FLAG_LISTEN_IF_RESUMED;
-            if ((mFlags & FLAG_RESUMED) != 0) {
-                // If we are resumed, start listening immediately. Note we do not check for
-                // duplicate calls before calling startListening as startListening is safe to call
-                // multiple times.
-                startListening();
-            }
-        } else {
-            mFlags &= ~FLAG_LISTEN_IF_RESUMED;
-            stopListening();
-        }
+    public void setStateIsNormal(boolean isNormal) {
+        setShouldListenFlag(FLAG_STATE_IS_NORMAL, isNormal);
+    }
+
+    /**
+     * Registers an "activity started/stopped" event.
+     */
+    public void setActivityStarted(boolean isStarted) {
+        setShouldListenFlag(FLAG_ACTIVITY_STARTED, isStarted);
+    }
+
+    /**
+     * Registers an "activity paused/resumed" event.
+     */
+    public void setActivityResumed(boolean isResumed) {
+        setShouldListenFlag(FLAG_ACTIVITY_RESUMED, isResumed);
     }
 
     @Override
@@ -200,7 +200,7 @@
         }
     }
 
-    void addPendingView(int appWidgetId, PendingAppWidgetHostView view) {
+    public void addPendingView(int appWidgetId, PendingAppWidgetHostView view) {
         mPendingViews.put(appWidgetId, view);
     }
 
@@ -248,7 +248,7 @@
         super.onProviderChanged(appWidgetId, info);
         // The super method updates the dimensions of the providerInfo. Update the
         // launcher spans accordingly.
-        info.initSpans(mContext);
+        info.initSpans(mContext, LauncherAppState.getIDP(mContext));
     }
 
     /**
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 780a1a1..3285c18 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -34,7 +34,6 @@
 
 import com.android.launcher3.CheckLongPressHelper;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dragndrop.DragLayer;
@@ -70,8 +69,6 @@
     private boolean mIsAutoAdvanceRegistered;
     private Runnable mAutoAdvanceRunnable;
 
-
-
     public LauncherAppWidgetHostView(Context context) {
         super(context);
         mLauncher = Launcher.getLauncher(context);
diff --git a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
similarity index 86%
rename from src/com/android/launcher3/LauncherAppWidgetProviderInfo.java
rename to src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
index 618b5de..ce97d2e 100644
--- a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
@@ -1,4 +1,4 @@
-package com.android.launcher3;
+package com.android.launcher3.widget;
 
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetProviderInfo;
@@ -11,6 +11,9 @@
 import android.os.Parcel;
 import android.os.UserHandle;
 
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.ComponentWithLabelAndIcon;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -48,7 +51,7 @@
             launcherInfo = new LauncherAppWidgetProviderInfo(p);
             p.recycle();
         }
-        launcherInfo.initSpans(context);
+        launcherInfo.initSpans(context, LauncherAppState.getIDP(context));
         return launcherInfo;
     }
 
@@ -58,9 +61,7 @@
         super(in);
     }
 
-    public void initSpans(Context context) {
-        InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
-
+    public void initSpans(Context context, InvariantDeviceProfile idp) {
         Point landCellSize = idp.landscapeProfile.getCellSize();
         Point portCellSize = idp.portraitProfile.getCellSize();
 
@@ -71,8 +72,12 @@
 
         // We want to account for the extra amount of padding that we are adding to the widget
         // to ensure that it gets the full amount of space that it has requested.
-        Rect widgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(
-                context, provider, null);
+        // If grids supports insetting widgets, we do not account for widget padding.
+        Rect widgetPadding = new Rect();
+        if (!idp.landscapeProfile.shouldInsetWidgets()
+                || !idp.portraitProfile.shouldInsetWidgets()) {
+            AppWidgetHostView.getDefaultPaddingForWidget(context, provider, widgetPadding);
+        }
         spanX = Math.max(1, (int) Math.ceil(
                         (minWidth + widgetPadding.left + widgetPadding.right) / smallestCellWidth));
         spanY = Math.max(1, (int) Math.ceil(
diff --git a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
index ed42bc4..6163b51 100644
--- a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.widget;
 
 import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
 import android.graphics.PointF;
 import android.graphics.Rect;
@@ -25,8 +26,11 @@
 import android.view.ViewDebug;
 import android.view.ViewGroup;
 
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Reorderable;
 import com.android.launcher3.dragndrop.DraggableView;
+import com.android.launcher3.views.ActivityContext;
 
 import java.util.ArrayList;
 
@@ -50,11 +54,16 @@
     private final PointF mTranslationForReorderPreview = new PointF(0, 0);
     private float mScaleForReorderBounce = 1f;
 
+    private final Rect mTempRect = new Rect();
+
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mChildrenFocused;
 
+    protected final BaseActivity mActivity;
+
     public NavigableAppWidgetHostView(Context context) {
         super(context);
+        mActivity = ActivityContext.lookupContext(context);
     }
 
     @Override
@@ -222,6 +231,25 @@
         int width = (int) (getMeasuredWidth() * mScaleToFit);
         int height = (int) (getMeasuredHeight() * mScaleToFit);
 
-        bounds.set(0, 0 , width, height);
+        getWidgetInset(mActivity.getDeviceProfile(), mTempRect);
+        bounds.set(mTempRect.left, mTempRect.top, width - mTempRect.right,
+                height - mTempRect.bottom);
+    }
+
+    /**
+     * Widgets have padding added by the system. We may choose to inset this padding if the grid
+     * supports it.
+     */
+    public void getWidgetInset(DeviceProfile grid, Rect out) {
+        if (!grid.shouldInsetWidgets()) {
+            out.setEmpty();
+            return;
+        }
+        AppWidgetProviderInfo info = getAppWidgetInfo();
+        if (info == null) {
+            out.set(grid.inv.defaultWidgetPadding);
+        } else {
+            AppWidgetHostView.getDefaultPaddingForWidget(getContext(), info.provider, out);
+        }
     }
 }
diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
index bef9a08..ee0b84e 100644
--- a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
+++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
@@ -20,7 +20,6 @@
 import android.appwidget.AppWidgetHostView;
 import android.os.Bundle;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.PendingAddItemInfo;
 
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index ca47728..4b113d8 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -29,6 +29,7 @@
 import android.text.Layout;
 import android.text.StaticLayout;
 import android.text.TextPaint;
+import android.util.SizeF;
 import android.util.TypedValue;
 import android.view.ContextThemeWrapper;
 import android.view.View;
@@ -46,6 +47,8 @@
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.util.Themes;
 
+import java.util.List;
+
 public class PendingAppWidgetHostView extends LauncherAppWidgetHostView
         implements OnClickListener, ItemInfoUpdateReceiver {
     private static final float SETUP_ICON_SIZE_FACTOR = 2f / 5;
@@ -109,6 +112,11 @@
     }
 
     @Override
+    public void updateAppWidgetSize(Bundle newOptions, List<SizeF> sizes) {
+        // No-op
+    }
+
+    @Override
     protected View getDefaultView() {
         View defaultView = mInflater.inflate(R.layout.appwidget_not_ready, this, false);
         defaultView.setOnClickListener(this);
diff --git a/src/com/android/launcher3/widget/PendingItemDragHelper.java b/src/com/android/launcher3/widget/PendingItemDragHelper.java
index 3c11274..6e83836 100644
--- a/src/com/android/launcher3/widget/PendingItemDragHelper.java
+++ b/src/com/android/launcher3/widget/PendingItemDragHelper.java
@@ -25,6 +25,8 @@
 import android.view.View;
 import android.widget.RemoteViews;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.Launcher;
@@ -33,7 +35,6 @@
 import com.android.launcher3.R;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DraggableView;
-import com.android.launcher3.dragndrop.LivePreviewWidgetCell;
 import com.android.launcher3.graphics.DragPreviewProvider;
 import com.android.launcher3.icons.LauncherIcons;
 
@@ -48,14 +49,14 @@
     private final PendingAddItemInfo mAddInfo;
     private int[] mEstimatedCellSize;
 
-    private RemoteViews mPreview;
+    @Nullable private RemoteViews mPreview;
 
     public PendingItemDragHelper(View view) {
         super(view);
         mAddInfo = (PendingAddItemInfo) view.getTag();
     }
 
-    public void setPreview(RemoteViews preview) {
+    public void setPreview(@Nullable RemoteViews preview) {
         mPreview = preview;
     }
 
@@ -90,7 +91,7 @@
             int[] previewSizeBeforeScale = new int[1];
 
             if (mPreview != null) {
-                preview = LivePreviewWidgetCell.generateFromRemoteViews(launcher, mPreview,
+                preview = WidgetCell.generateFromRemoteViews(launcher, mPreview,
                         createWidgetInfo.info, maxWidth, previewSizeBeforeScale);
             }
             if (preview == null) {
diff --git a/src/com/android/launcher3/widget/WidgetAddFlowHandler.java b/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
index ebc2a25..1ac5a33 100644
--- a/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
+++ b/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
@@ -21,7 +21,6 @@
 import android.os.Parcelable;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.util.PendingRequestArgs;
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index bef91d2..229df50 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -16,7 +16,11 @@
 
 package com.android.launcher3.widget;
 
+import static com.android.launcher3.Utilities.ATLEAST_S;
+
+import android.appwidget.AppWidgetHostView;
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.os.CancellationSignal;
 import android.util.AttributeSet;
@@ -24,10 +28,11 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnLayoutChangeListener;
-import android.view.ViewGroup;
 import android.view.ViewPropertyAnimator;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.FrameLayout;
 import android.widget.LinearLayout;
+import android.widget.RemoteViews;
 import android.widget.TextView;
 
 import com.android.launcher3.BaseActivity;
@@ -36,6 +41,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.WidgetPreviewLoader;
 import com.android.launcher3.icons.BaseIconFactory;
+import com.android.launcher3.icons.BitmapRenderer;
 import com.android.launcher3.model.WidgetItem;
 
 /**
@@ -60,12 +66,15 @@
     /** Widget preview width is calculated by multiplying this factor to the widget cell width. */
     private static final float PREVIEW_SCALE = 0.8f;
 
+    protected int mPreviewWidth;
+    protected int mPreviewHeight;
     protected int mPresetPreviewSize;
     private int mCellSize;
 
     private WidgetImageView mWidgetImage;
     private TextView mWidgetName;
     private TextView mWidgetDims;
+    private TextView mWidgetDescription;
 
     protected WidgetItem mItem;
 
@@ -81,6 +90,9 @@
     protected final DeviceProfile mDeviceProfile;
     private final CheckLongPressHelper mLongPressHelper;
 
+    private RemoteViews mPreview;
+    private AppWidgetHostView mPreviewAppWidgetHostView;
+
     public WidgetCell(Context context) {
         this(context, null);
     }
@@ -106,15 +118,25 @@
     private void setContainerWidth() {
         mCellSize = (int) (mDeviceProfile.allAppsIconSizePx * WIDTH_SCALE);
         mPresetPreviewSize = (int) (mCellSize * PREVIEW_SCALE);
+        mPreviewWidth = mPreviewHeight = mPresetPreviewSize;
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
 
-        mWidgetImage = (WidgetImageView) findViewById(R.id.widget_preview);
-        mWidgetName = ((TextView) findViewById(R.id.widget_name));
-        mWidgetDims = ((TextView) findViewById(R.id.widget_dims));
+        mWidgetImage = findViewById(R.id.widget_preview);
+        mWidgetName = findViewById(R.id.widget_name);
+        mWidgetDims = findViewById(R.id.widget_dims);
+        mWidgetDescription = findViewById(R.id.widget_description);
+    }
+
+    public void setPreview(RemoteViews view) {
+        mPreview = view;
+    }
+
+    public RemoteViews getPreview() {
+        return mPreview;
     }
 
     /**
@@ -128,22 +150,38 @@
         mWidgetImage.setBitmap(null, null);
         mWidgetName.setText(null);
         mWidgetDims.setText(null);
+        mWidgetDescription.setText(null);
+        mWidgetDescription.setVisibility(GONE);
+        mPreviewWidth = mPreviewHeight = mPresetPreviewSize;
 
         if (mActiveRequest != null) {
             mActiveRequest.cancel();
             mActiveRequest = null;
         }
+        mPreview = null;
+        mPreviewAppWidgetHostView = null;
     }
 
     public void applyFromCellItem(WidgetItem item, WidgetPreviewLoader loader) {
+        applyPreviewLayout(item);
+
         mItem = item;
         mWidgetName.setText(mItem.label);
         mWidgetDims.setText(getContext().getString(R.string.widget_dims_format,
                 mItem.spanX, mItem.spanY));
         mWidgetDims.setContentDescription(getContext().getString(
                 R.string.widget_accessible_dims_format, mItem.spanX, mItem.spanY));
-        mWidgetPreviewLoader = loader;
+        if (ATLEAST_S && mItem.widgetInfo != null) {
+            CharSequence description = mItem.widgetInfo.loadDescription(getContext());
+            if (description != null && description.length() > 0) {
+                mWidgetDescription.setText(description);
+                mWidgetDescription.setVisibility(VISIBLE);
+            } else {
+                mWidgetDescription.setVisibility(GONE);
+            }
+        }
 
+        mWidgetPreviewLoader = loader;
         if (item.activityInfo != null) {
             setTag(new PendingAddShortcutInfo(item.activityInfo));
         } else {
@@ -151,6 +189,27 @@
         }
     }
 
+    private void applyPreviewLayout(WidgetItem item) {
+        if (ATLEAST_S
+                && mPreview == null
+                && item.widgetInfo != null
+                && item.widgetInfo.previewLayout != Resources.ID_NULL) {
+            mPreviewAppWidgetHostView = new AppWidgetHostView(getContext());
+            LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo =
+                    LauncherAppWidgetProviderInfo.fromProviderInfo(getContext(),
+                            item.widgetInfo.clone());
+            // A hack to force the initial layout to be the preview layout since there is no API for
+            // rendering a preview layout for work profile apps yet. For non-work profile layout, a
+            // proper solution is to use RemoteViews(PackageName, LayoutId).
+            launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout;
+            mPreviewAppWidgetHostView.setAppWidget(/* appWidgetId= */ -1,
+                    launcherAppWidgetProviderInfo);
+            mPreviewAppWidgetHostView.setPadding(/* left= */ 0, /* top= */0, /* right= */
+                    0, /* bottom= */ 0);
+            mPreviewAppWidgetHostView.updateAppWidget(/* remoteViews= */ null);
+        }
+    }
+
     public WidgetImageView getWidgetView() {
         return mWidgetImage;
     }
@@ -181,6 +240,11 @@
             return;
         }
         if (bitmap != null) {
+            LayoutParams layoutParams = (LayoutParams) mWidgetImage.getLayoutParams();
+            layoutParams.width = bitmap.getWidth();
+            layoutParams.height = bitmap.getHeight();
+            mWidgetImage.setLayoutParams(layoutParams);
+
             mWidgetImage.setBitmap(bitmap, mWidgetPreviewLoader.getBadgeForUser(mItem.user,
                     BaseIconFactory.getBadgeSizeForIconSize(mDeviceProfile.allAppsIconSizePx)));
             if (mAnimatePreview) {
@@ -194,11 +258,36 @@
     }
 
     public void ensurePreview() {
+        if (mPreview != null && mActiveRequest == null) {
+            Bitmap preview = generateFromRemoteViews(
+                    mActivity, mPreview, mItem.widgetInfo, mPresetPreviewSize, new int[1]);
+            if (preview != null) {
+                applyPreview(preview);
+                return;
+            }
+        }
+
+        if (mPreviewAppWidgetHostView != null) {
+            Bitmap preview = generateFromView(mActivity, mPreviewAppWidgetHostView,
+                    mItem.widgetInfo, mPreviewWidth, new int[1]);
+            if (preview != null) {
+                applyPreview(preview);
+                return;
+            }
+        }
         if (mActiveRequest != null) {
             return;
         }
-        mActiveRequest = mWidgetPreviewLoader.getPreview(
-                mItem, mPresetPreviewSize, mPresetPreviewSize, this);
+        mActiveRequest = mWidgetPreviewLoader.getPreview(mItem, mPreviewWidth, mPreviewHeight,
+                this);
+    }
+
+    /** Sets the widget preview image size in number of cells. */
+    public void setPreviewSize(int spanX, int spanY) {
+        int padding = 2 * getResources()
+                .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
+        mPreviewWidth = mDeviceProfile.cellWidthPx * spanX + padding;
+        mPreviewHeight = mDeviceProfile.cellHeightPx * spanY + padding;
     }
 
     @Override
@@ -233,12 +322,6 @@
     }
 
     @Override
-    public void setLayoutParams(ViewGroup.LayoutParams params) {
-        params.width = params.height = mCellSize;
-        super.setLayoutParams(params);
-    }
-
-    @Override
     public CharSequence getAccessibilityClassName() {
         return WidgetCell.class.getName();
     }
@@ -248,4 +331,53 @@
         super.onInitializeAccessibilityNodeInfo(info);
         info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
     }
+
+    /**
+     * Generates a bitmap by inflating {@param views}.
+     * @see com.android.launcher3.WidgetPreviewLoader#generateWidgetPreview
+     *
+     * TODO: Consider moving this to the background thread.
+     */
+    public static Bitmap generateFromRemoteViews(BaseActivity activity, RemoteViews views,
+            LauncherAppWidgetProviderInfo info, int previewSize, int[] preScaledWidthOut) {
+        try {
+            return generateFromView(activity, views.apply(activity, new FrameLayout(activity)),
+                    info, previewSize, preScaledWidthOut);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private static Bitmap generateFromView(BaseActivity activity, View v,
+            LauncherAppWidgetProviderInfo info, int previewSize, int[] preScaledWidthOut) {
+
+        DeviceProfile dp = activity.getDeviceProfile();
+        int viewWidth = dp.cellWidthPx * info.spanX;
+        int viewHeight = dp.cellHeightPx * info.spanY;
+
+        v.measure(MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY),
+                MeasureSpec.makeMeasureSpec(viewHeight, MeasureSpec.EXACTLY));
+
+        viewWidth = v.getMeasuredWidth();
+        viewHeight = v.getMeasuredHeight();
+        v.layout(0, 0, viewWidth, viewHeight);
+
+        preScaledWidthOut[0] = viewWidth;
+        final int bitmapWidth, bitmapHeight;
+        final float scale;
+        if (viewWidth > previewSize) {
+            scale = ((float) previewSize) / viewWidth;
+            bitmapWidth = previewSize;
+            bitmapHeight = (int) (viewHeight * scale);
+        } else {
+            scale = 1;
+            bitmapWidth = viewWidth;
+            bitmapHeight = viewHeight;
+        }
+
+        return BitmapRenderer.createSoftwareBitmap(bitmapWidth, bitmapHeight, c -> {
+            c.scale(scale, scale);
+            v.draw(c);
+        });
+    }
 }
diff --git a/src/com/android/launcher3/widget/WidgetHostViewLoader.java b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
index c022374..12e0d43 100644
--- a/src/com/android/launcher3/widget/WidgetHostViewLoader.java
+++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
@@ -7,17 +7,20 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.util.Log;
+import android.util.SizeF;
 import android.view.View;
 
 import com.android.launcher3.AppWidgetResizeFrame;
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.util.Thunk;
 
+import java.util.ArrayList;
+import java.util.stream.Collectors;
+
 public class WidgetHostViewLoader implements DragController.DragListener {
     private static final String TAG = "WidgetHostViewLoader";
     private static final boolean LOGD = false;
@@ -152,24 +155,28 @@
     }
 
     public static Bundle getDefaultOptionsForWidget(Context context, PendingAddWidgetInfo info) {
-        Rect rect = new Rect();
-        AppWidgetResizeFrame.getWidgetSizeRanges(context, info.spanX, info.spanY, rect);
+        ArrayList<SizeF> sizes = AppWidgetResizeFrame
+                .getWidgetSizes(context, info.spanX, info.spanY);
+
         Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(context,
                 info.componentName, null);
-
         float density = context.getResources().getDisplayMetrics().density;
-        int xPaddingDips = (int) ((padding.left + padding.right) / density);
-        int yPaddingDips = (int) ((padding.top + padding.bottom) / density);
+        float xPaddingDips = (padding.left + padding.right) / density;
+        float yPaddingDips = (padding.top + padding.bottom) / density;
+
+        ArrayList<SizeF> paddedSizes = sizes.stream().map(
+                size -> new SizeF(Math.max(0.f, size.getWidth() - xPaddingDips),
+                        Math.max(0.f, size.getHeight() - yPaddingDips))).collect(
+                Collectors.toCollection(ArrayList::new));
+
+        Rect rect = AppWidgetResizeFrame.getMinMaxSizes(paddedSizes, null /* outRect */);
 
         Bundle options = new Bundle();
-        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH,
-                rect.left - xPaddingDips);
-        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT,
-                rect.top - yPaddingDips);
-        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH,
-                rect.right - xPaddingDips);
-        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT,
-                rect.bottom - yPaddingDips);
+        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, rect.left);
+        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, rect.top);
+        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, rect.right);
+        options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, rect.bottom);
+        options.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, paddedSizes);
         return options;
     }
 }
diff --git a/src/com/android/launcher3/widget/WidgetListRowEntry.java b/src/com/android/launcher3/widget/WidgetListRowEntry.java
deleted file mode 100644
index 17e4673..0000000
--- a/src/com/android/launcher3/widget/WidgetListRowEntry.java
+++ /dev/null
@@ -1,48 +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.widget;
-
-import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.PackageItemInfo;
-
-import java.util.ArrayList;
-
-/**
- * Holder class to store all the information related to a single row in the widget list
- */
-public class WidgetListRowEntry {
-
-    public final PackageItemInfo pkgItem;
-
-    public final ArrayList<WidgetItem> widgets;
-
-    /**
-     * Character that is used as a section name for the {@link ItemInfo#title}.
-     * (e.g., "G" will be stored if title is "Google")
-     */
-    public String titleSectionName;
-
-    public WidgetListRowEntry(PackageItemInfo pkgItem, ArrayList<WidgetItem> items) {
-        this.pkgItem = pkgItem;
-        this.widgets = items;
-    }
-
-    @Override
-    public String toString() {
-        return pkgItem.packageName + ":" + widgets.size();
-    }
-}
diff --git a/src/com/android/launcher3/widget/WidgetManagerHelper.java b/src/com/android/launcher3/widget/WidgetManagerHelper.java
index c0c5c48..15fa844 100644
--- a/src/com/android/launcher3/widget/WidgetManagerHelper.java
+++ b/src/com/android/launcher3/widget/WidgetManagerHelper.java
@@ -27,18 +27,14 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -122,15 +118,6 @@
                 appWidgetId).getBoolean(WIDGET_OPTION_RESTORE_COMPLETED);
     }
 
-    public static Map<ComponentKey, AppWidgetProviderInfo> getAllProvidersMap(Context context) {
-        if (WidgetsModel.GO_DISABLE_WIDGETS) {
-            return Collections.emptyMap();
-        }
-        return allWidgetsSteam(context).collect(
-                        Collectors.toMap(info -> new ComponentKey(info.provider, info.getProfile()),
-                        Function.identity()));
-    }
-
     private static Stream<AppWidgetProviderInfo> allWidgetsSteam(Context context) {
         AppWidgetManager awm = context.getSystemService(AppWidgetManager.class);
         return Stream.concat(
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index 3585a18..e6d54a9 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -29,16 +29,20 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.animation.Interpolator;
+import android.widget.ScrollView;
+import android.widget.TableLayout;
+import android.widget.TableRow;
 import android.widget.TextView;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
-import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.util.WidgetsTableUtils;
 
 import java.util.List;
 
@@ -64,6 +68,8 @@
     private static final int DEFAULT_CLOSE_DURATION = 200;
     private ItemInfo mOriginalItemInfo;
     private Rect mInsets;
+    private final int mMaxTableHeight;
+    private int mMaxHorizontalSpan = 4;
 
     public WidgetsBottomSheet(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
@@ -74,12 +80,41 @@
         setWillNotDraw(false);
         mInsets = new Rect();
         mContent = this;
+        DeviceProfile deviceProfile = mLauncher.getDeviceProfile();
+        // Set the max table height to 2 / 3 of the grid height so that the bottom picker won't
+        // take over the entire view vertically.
+        mMaxTableHeight = deviceProfile.inv.numRows * 2 / 3  * deviceProfile.cellHeightPx;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        int paddingPx = 2 * getResources().getDimensionPixelOffset(
+                R.dimen.widget_cell_horizontal_padding);
+        int maxHorizontalSpan = findViewById(R.id.widgets_table).getMeasuredWidth()
+                / (mLauncher.getDeviceProfile().cellWidthPx + paddingPx);
+
+        if (mMaxHorizontalSpan != maxHorizontalSpan) {
+            // Ensure the table layout is showing widgets in the right column after measure.
+            mMaxHorizontalSpan = maxHorizontalSpan;
+            onWidgetsBound();
+        }
     }
 
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         super.onLayout(changed, l, t, r, b);
         setTranslationShift(mTranslationShift);
+
+        // Ensure the scroll view height is not larger than mMaxTableHeight, which is a value
+        // smaller than the entire screen height.
+        ScrollView widgetsTableScrollView = findViewById(R.id.widgets_table_scroll_view);
+        if (widgetsTableScrollView.getMeasuredHeight() > mMaxTableHeight) {
+            ViewGroup.LayoutParams layoutParams = widgetsTableScrollView.getLayoutParams();
+            layoutParams.height = mMaxTableHeight;
+            widgetsTableScrollView.setLayoutParams(layoutParams);
+        }
     }
 
     public void populateAndShow(ItemInfo itemInfo) {
@@ -100,47 +135,31 @@
                         mOriginalItemInfo.getTargetComponent().getPackageName(),
                         mOriginalItemInfo.user));
 
-        ViewGroup widgetRow = findViewById(R.id.widgets);
-        ViewGroup widgetCells = widgetRow.findViewById(R.id.widgets_cell_list);
+        TableLayout widgetsTable = findViewById(R.id.widgets_table);
+        widgetsTable.removeAllViews();
 
-        widgetCells.removeAllViews();
-
-        for (int i = 0; i < widgets.size(); i++) {
-            WidgetCell widget = addItemCell(widgetCells);
-            widget.applyFromCellItem(widgets.get(i), LauncherAppState.getInstance(mLauncher)
-                    .getWidgetCache());
-            widget.ensurePreview();
-            widget.setVisibility(View.VISIBLE);
-            if (i < widgets.size() - 1) {
-                addDivider(widgetCells);
-            }
-        }
-
-        if (widgets.size() == 1) {
-            // If there is only one widget, we want to center it instead of left-align.
-            WidgetsBottomSheet.LayoutParams params = (WidgetsBottomSheet.LayoutParams)
-                    widgetRow.getLayoutParams();
-            params.gravity = Gravity.CENTER_HORIZONTAL;
-        } else {
-            // Otherwise, add an empty view to the start as padding (but still scroll edge to edge).
-            View leftPaddingView = LayoutInflater.from(getContext()).inflate(
-                    R.layout.widget_list_divider, widgetRow, false);
-            leftPaddingView.getLayoutParams().width = ResourceUtils.pxFromDp(
-                    16, getResources().getDisplayMetrics());
-            widgetCells.addView(leftPaddingView, 0);
-        }
-    }
-
-    private void addDivider(ViewGroup parent) {
-        LayoutInflater.from(getContext()).inflate(R.layout.widget_list_divider, parent, true);
+        WidgetsTableUtils.groupWidgetItemsIntoTable(widgets, mMaxHorizontalSpan).forEach(row -> {
+            TableRow tableRow = new TableRow(getContext());
+            tableRow.setGravity(Gravity.CENTER_VERTICAL);
+            row.forEach(widgetItem -> {
+                WidgetCell widget = addItemCell(tableRow);
+                widget.setPreviewSize(widgetItem.spanX, widgetItem.spanY);
+                widget.applyFromCellItem(widgetItem, LauncherAppState.getInstance(mLauncher)
+                        .getWidgetCache());
+                widget.ensurePreview();
+                widget.setVisibility(View.VISIBLE);
+            });
+            widgetsTable.addView(tableRow);
+        });
     }
 
     protected WidgetCell addItemCell(ViewGroup parent) {
-        WidgetCell widget = (WidgetCell) LayoutInflater.from(getContext()).inflate(
-                R.layout.widget_cell, parent, false);
+        WidgetCell widget = (WidgetCell) LayoutInflater.from(getContext())
+                .inflate(R.layout.widget_cell, parent, false);
 
-        widget.setOnClickListener(this);
-        widget.setOnLongClickListener(this);
+        WidgetImageView preview = widget.findViewById(R.id.widget_preview);
+        preview.setOnClickListener(this);
+        preview.setOnLongClickListener(this);
         widget.setAnimatePreview(false);
 
         parent.addView(widget);
diff --git a/src/com/android/launcher3/widget/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
deleted file mode 100644
index df6e2c3..0000000
--- a/src/com/android/launcher3/widget/WidgetsDiffReporter.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.launcher3.widget;
-
-import android.util.Log;
-
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.model.data.PackageItemInfo;
-import com.android.launcher3.widget.WidgetsListAdapter.WidgetListRowEntryComparator;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-
-/**
- * Do diff on widget's tray list items and call the {@link RecyclerView.Adapter}
- * methods accordingly.
- */
-public class WidgetsDiffReporter {
-    private static final boolean DEBUG = false;
-    private static final String TAG = "WidgetsDiffReporter";
-
-    private final IconCache mIconCache;
-    private final RecyclerView.Adapter mListener;
-
-    public WidgetsDiffReporter(IconCache iconCache, RecyclerView.Adapter listener) {
-        mIconCache = iconCache;
-        mListener = listener;
-    }
-
-    public void process(ArrayList<WidgetListRowEntry> currentEntries,
-            ArrayList<WidgetListRowEntry> newEntries, WidgetListRowEntryComparator comparator) {
-        if (DEBUG) {
-            Log.d(TAG, "process oldEntries#=" + currentEntries.size()
-                    + " newEntries#=" + newEntries.size());
-        }
-        // Early exit if either of the list is empty
-        if (currentEntries.isEmpty() || newEntries.isEmpty()) {
-            // Skip if both list are empty.
-            // On rotation, we open the widget tray with empty. Then try to fetch the list again
-            // when the animation completes (which still gives empty). And we get the final result
-            // when the bind actually completes.
-            if (currentEntries.size() != newEntries.size()) {
-                currentEntries.clear();
-                currentEntries.addAll(newEntries);
-                mListener.notifyDataSetChanged();
-            }
-            return;
-        }
-        ArrayList<WidgetListRowEntry> orgEntries =
-                (ArrayList<WidgetListRowEntry>) currentEntries.clone();
-        Iterator<WidgetListRowEntry> orgIter = orgEntries.iterator();
-        Iterator<WidgetListRowEntry> newIter = newEntries.iterator();
-
-        WidgetListRowEntry orgRowEntry = orgIter.next();
-        WidgetListRowEntry newRowEntry = newIter.next();
-
-        do {
-            int diff = comparePackageName(orgRowEntry, newRowEntry, comparator);
-            if (DEBUG) {
-                Log.d(TAG, String.format("diff=%d orgRowEntry (%s) newRowEntry (%s)",
-                        diff, orgRowEntry != null? orgRowEntry.toString() : null,
-                        newRowEntry != null? newRowEntry.toString() : null));
-            }
-            int index = -1;
-            if (diff < 0) {
-                index = currentEntries.indexOf(orgRowEntry);
-                mListener.notifyItemRemoved(index);
-                if (DEBUG) {
-                    Log.d(TAG, String.format("notifyItemRemoved called (%d)%s", index,
-                            orgRowEntry.titleSectionName));
-                }
-                currentEntries.remove(index);
-                orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
-            } else if (diff > 0) {
-                index = orgRowEntry != null? currentEntries.indexOf(orgRowEntry):
-                        currentEntries.size();
-                currentEntries.add(index, newRowEntry);
-                if (DEBUG) {
-                    Log.d(TAG, String.format("notifyItemInserted called (%d)%s", index,
-                            newRowEntry.titleSectionName));
-                }
-                newRowEntry = newIter.hasNext() ? newIter.next() : null;
-                mListener.notifyItemInserted(index);
-
-            } else {
-                // same package name but,
-                // did the icon, title, etc, change?
-                // or did the widget size and desc, span, etc change?
-                if (!isSamePackageItemInfo(orgRowEntry.pkgItem, newRowEntry.pkgItem) ||
-                        !orgRowEntry.widgets.equals(newRowEntry.widgets)) {
-                    index = currentEntries.indexOf(orgRowEntry);
-                    currentEntries.set(index, newRowEntry);
-                    mListener.notifyItemChanged(index);
-                    if (DEBUG) {
-                        Log.d(TAG, String.format("notifyItemChanged called (%d)%s", index,
-                                newRowEntry.titleSectionName));
-                    }
-                }
-                orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
-                newRowEntry = newIter.hasNext() ? newIter.next() : null;
-            }
-        } while(orgRowEntry != null || newRowEntry != null);
-    }
-
-    /**
-     * Compare package name using the same comparator as in {@link WidgetsListAdapter}.
-     * Also handle null row pointers.
-     */
-    private int comparePackageName(WidgetListRowEntry curRow, WidgetListRowEntry newRow,
-            WidgetListRowEntryComparator comparator) {
-        if (curRow == null && newRow == null) {
-            throw new IllegalStateException("Cannot compare PackageItemInfo if both rows are null.");
-        }
-
-        if (curRow == null && newRow != null) {
-            return 1; // new row needs to be inserted
-        } else if (curRow != null && newRow == null) {
-            return -1; // old row needs to be deleted
-        }
-        return comparator.compare(curRow, newRow);
-    }
-
-    private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) {
-        return curInfo.bitmap.icon.equals(newInfo.bitmap.icon)
-                && !mIconCache.isDefaultIcon(curInfo.bitmap, curInfo.user);
-    }
-}
diff --git a/src/com/android/launcher3/widget/WidgetsFullSheet.java b/src/com/android/launcher3/widget/WidgetsFullSheet.java
deleted file mode 100644
index 4c8c339..0000000
--- a/src/com/android/launcher3/widget/WidgetsFullSheet.java
+++ /dev/null
@@ -1,257 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.widget;
-
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
-import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.PropertyValuesHolder;
-import android.content.Context;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.util.Pair;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.animation.AnimationUtils;
-import android.view.animation.Interpolator;
-
-import androidx.annotation.VisibleForTesting;
-
-import com.android.launcher3.Insettable;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetHost.ProviderChangedListener;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.views.RecyclerViewFastScroller;
-import com.android.launcher3.views.TopRoundedCornerView;
-
-/**
- * Popup for showing the full list of available widgets
- */
-public class WidgetsFullSheet extends BaseWidgetSheet
-        implements Insettable, ProviderChangedListener {
-
-    private static final long DEFAULT_OPEN_DURATION = 267;
-    private static final long FADE_IN_DURATION = 150;
-    private static final float VERTICAL_START_POSITION = 0.3f;
-
-    private final Rect mInsets = new Rect();
-
-    private final WidgetsListAdapter mAdapter;
-
-    private WidgetsRecyclerView mRecyclerView;
-
-    public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        LauncherAppState apps = LauncherAppState.getInstance(context);
-        mAdapter = new WidgetsListAdapter(context,
-                LayoutInflater.from(context), apps.getWidgetCache(), apps.getIconCache(),
-                this, this);
-
-    }
-
-    public WidgetsFullSheet(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mContent = findViewById(R.id.container);
-
-        mRecyclerView = findViewById(R.id.widgets_list_view);
-        mRecyclerView.setAdapter(mAdapter);
-        mAdapter.setApplyBitmapDeferred(true, mRecyclerView);
-
-        TopRoundedCornerView springLayout = (TopRoundedCornerView) mContent;
-        springLayout.addSpringView(R.id.widgets_list_view);
-        mRecyclerView.setEdgeEffectFactory(springLayout.createEdgeEffectFactory());
-        onWidgetsBound();
-    }
-
-    @VisibleForTesting
-    public WidgetsRecyclerView getRecyclerView() {
-        return mRecyclerView;
-    }
-
-    @Override
-    protected Pair<View, String> getAccessibilityTarget() {
-        return Pair.create(mRecyclerView, getContext().getString(
-                mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed));
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        mLauncher.getAppWidgetHost().addProviderChangeListener(this);
-        notifyWidgetProvidersChanged();
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        mLauncher.getAppWidgetHost().removeProviderChangeListener(this);
-    }
-
-    @Override
-    public void setInsets(Rect insets) {
-        mInsets.set(insets);
-
-        mRecyclerView.setPadding(
-                mRecyclerView.getPaddingLeft(), mRecyclerView.getPaddingTop(),
-                mRecyclerView.getPaddingRight(), insets.bottom);
-        if (insets.bottom > 0) {
-            setupNavBarColor();
-        } else {
-            clearNavBarColor();
-        }
-
-        ((TopRoundedCornerView) mContent).setNavBarScrimHeight(mInsets.bottom);
-        requestLayout();
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        int widthUsed;
-        if (mInsets.bottom > 0) {
-            widthUsed = mInsets.left + mInsets.right;
-        } else {
-            Rect padding = mLauncher.getDeviceProfile().workspacePadding;
-            widthUsed = Math.max(padding.left + padding.right,
-                    2 * (mInsets.left + mInsets.right));
-        }
-
-        int heightUsed = mInsets.top + mLauncher.getDeviceProfile().edgeMarginPx;
-        measureChildWithMargins(mContent, widthMeasureSpec,
-                widthUsed, heightMeasureSpec, heightUsed);
-        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
-                MeasureSpec.getSize(heightMeasureSpec));
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        int width = r - l;
-        int height = b - t;
-
-        // Content is laid out as center bottom aligned
-        int contentWidth = mContent.getMeasuredWidth();
-        int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left;
-        mContent.layout(contentLeft, height - mContent.getMeasuredHeight(),
-                contentLeft + contentWidth, height);
-
-        setTranslationShift(mTranslationShift);
-    }
-
-    @Override
-    public void notifyWidgetProvidersChanged() {
-        mLauncher.refreshAndBindWidgetsForPackageUser(null);
-    }
-
-    @Override
-    public void onWidgetsBound() {
-        mAdapter.setWidgets(mLauncher.getPopupDataProvider().getAllWidgets());
-    }
-
-    private void open(boolean animate) {
-        if (animate) {
-            if (getPopupContainer().getInsets().bottom > 0) {
-                mContent.setAlpha(0);
-                setTranslationShift(VERTICAL_START_POSITION);
-            }
-            mOpenCloseAnimator.setValues(
-                    PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
-            mOpenCloseAnimator
-                    .setDuration(DEFAULT_OPEN_DURATION)
-                    .setInterpolator(AnimationUtils.loadInterpolator(
-                            getContext(), android.R.interpolator.linear_out_slow_in));
-            mRecyclerView.setLayoutFrozen(true);
-            mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    mRecyclerView.setLayoutFrozen(false);
-                    mAdapter.setApplyBitmapDeferred(false, mRecyclerView);
-                    mOpenCloseAnimator.removeListener(this);
-                }
-            });
-            post(() -> {
-                mOpenCloseAnimator.start();
-                mContent.animate().alpha(1).setDuration(FADE_IN_DURATION);
-            });
-        } else {
-            setTranslationShift(TRANSLATION_SHIFT_OPENED);
-            mAdapter.setApplyBitmapDeferred(false, mRecyclerView);
-            post(this::announceAccessibilityChanges);
-        }
-    }
-
-    @Override
-    protected void handleClose(boolean animate) {
-        handleClose(animate, DEFAULT_OPEN_DURATION);
-    }
-
-    @Override
-    protected boolean isOfType(int type) {
-        return (type & TYPE_WIDGETS_FULL_SHEET) != 0;
-    }
-
-    @Override
-    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        // Disable swipe down when recycler view is scrolling
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            mNoIntercept = false;
-            RecyclerViewFastScroller scroller = mRecyclerView.getScrollbar();
-            if (scroller.getThumbOffsetY() >= 0 &&
-                    getPopupContainer().isEventOverView(scroller, ev)) {
-                mNoIntercept = true;
-            } else if (getPopupContainer().isEventOverView(mContent, ev)) {
-                mNoIntercept = !mRecyclerView.shouldContainerScroll(ev, getPopupContainer());
-            }
-        }
-        return super.onControllerInterceptTouchEvent(ev);
-    }
-
-    public static WidgetsFullSheet show(Launcher launcher, boolean animate) {
-        WidgetsFullSheet sheet = (WidgetsFullSheet) launcher.getLayoutInflater()
-                .inflate(R.layout.widgets_full_sheet, launcher.getDragLayer(), false);
-        sheet.attachToContainer();
-        sheet.mIsOpen = true;
-        sheet.open(animate);
-        return sheet;
-    }
-
-    @VisibleForTesting
-    public static WidgetsRecyclerView getWidgetsView(Launcher launcher) {
-        return launcher.findViewById(R.id.widgets_list_view);
-    }
-
-    @Override
-    public void addHintCloseAnim(
-            float distanceToMove, Interpolator interpolator, PendingAnimation target) {
-        target.setFloat(mRecyclerView, VIEW_TRANSLATE_Y, -distanceToMove, interpolator);
-        target.setViewAlpha(mRecyclerView, 0.5f, interpolator);
-    }
-
-    @Override
-    protected void onCloseComplete() {
-        super.onCloseComplete();
-        AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL);
-    }
-}
diff --git a/src/com/android/launcher3/widget/WidgetsListAdapter.java b/src/com/android/launcher3/widget/WidgetsListAdapter.java
deleted file mode 100644
index 5bf9690..0000000
--- a/src/com/android/launcher3/widget/WidgetsListAdapter.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.widget;
-
-import android.content.Context;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.View.OnLongClickListener;
-import android.view.ViewGroup;
-
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerView.Adapter;
-
-import com.android.launcher3.R;
-import com.android.launcher3.WidgetPreviewLoader;
-import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.util.LabelComparator;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-/**
- * List view adapter for the widget tray.
- *
- * <p>Memory vs. Performance:
- * The less number of types of views are inserted into a {@link RecyclerView}, the more recycling
- * happens and less memory is consumed. {@link #getItemViewType} was not overridden as there is
- * only a single type of view.
- */
-public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> {
-
-    private static final String TAG = "WidgetsListAdapter";
-    private static final boolean DEBUG = false;
-
-    private final WidgetPreviewLoader mWidgetPreviewLoader;
-    private final LayoutInflater mLayoutInflater;
-
-    private final OnClickListener mIconClickListener;
-    private final OnLongClickListener mIconLongClickListener;
-    private final int mIndent;
-    private ArrayList<WidgetListRowEntry> mEntries = new ArrayList<>();
-    private final WidgetsDiffReporter mDiffReporter;
-
-    private boolean mApplyBitmapDeferred;
-
-    public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
-            WidgetPreviewLoader widgetPreviewLoader, IconCache iconCache,
-            OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) {
-        mLayoutInflater = layoutInflater;
-        mWidgetPreviewLoader = widgetPreviewLoader;
-        mIconClickListener = iconClickListener;
-        mIconLongClickListener = iconLongClickListener;
-        mIndent = context.getResources().getDimensionPixelSize(R.dimen.widget_section_indent);
-        mDiffReporter = new WidgetsDiffReporter(iconCache, this);
-    }
-
-    /**
-     * Defers applying bitmap on all the {@link WidgetCell} in the {@param rv}
-     *
-     * @see WidgetCell#setApplyBitmapDeferred(boolean)
-     */
-    public void setApplyBitmapDeferred(boolean isDeferred, RecyclerView rv) {
-        mApplyBitmapDeferred = isDeferred;
-
-        for (int i = rv.getChildCount() - 1; i >= 0; i--) {
-            WidgetsRowViewHolder holder = (WidgetsRowViewHolder)
-                    rv.getChildViewHolder(rv.getChildAt(i));
-            for (int j = holder.cellContainer.getChildCount() - 1; j >= 0; j--) {
-                View v = holder.cellContainer.getChildAt(j);
-                if (v instanceof WidgetCell) {
-                    ((WidgetCell) v).setApplyBitmapDeferred(mApplyBitmapDeferred);
-                }
-            }
-        }
-    }
-
-    /**
-     * Update the widget list.
-     */
-    public void setWidgets(ArrayList<WidgetListRowEntry> tempEntries) {
-        WidgetListRowEntryComparator rowComparator = new WidgetListRowEntryComparator();
-        Collections.sort(tempEntries, rowComparator);
-        mDiffReporter.process(mEntries, tempEntries, rowComparator);
-    }
-
-    @Override
-    public int getItemCount() {
-        return mEntries.size();
-    }
-
-    public String getSectionName(int pos) {
-        return mEntries.get(pos).titleSectionName;
-    }
-
-    @Override
-    public void onBindViewHolder(WidgetsRowViewHolder holder, int pos) {
-        WidgetListRowEntry entry = mEntries.get(pos);
-        List<WidgetItem> infoList = entry.widgets;
-
-        ViewGroup row = holder.cellContainer;
-        if (DEBUG) {
-            Log.d(TAG, String.format(
-                    "onBindViewHolder [pos=%d, widget#=%d, row.getChildCount=%d]",
-                    pos, infoList.size(), row.getChildCount()));
-        }
-
-        // Add more views.
-        // if there are too many, hide them.
-        int expectedChildCount = infoList.size() + Math.max(0, infoList.size() - 1);
-        int childCount = row.getChildCount();
-
-        if (expectedChildCount > childCount) {
-            for (int i = childCount; i < expectedChildCount; i++) {
-                if ((i & 1) == 1) {
-                    // Add a divider for odd index
-                    mLayoutInflater.inflate(R.layout.widget_list_divider, row);
-                } else {
-                    // Add cell for even index
-                    WidgetCell widget = (WidgetCell) mLayoutInflater.inflate(
-                            R.layout.widget_cell, row, false);
-
-                    // set up touch.
-                    widget.setOnClickListener(mIconClickListener);
-                    widget.setOnLongClickListener(mIconLongClickListener);
-                    row.addView(widget);
-                }
-            }
-        } else if (expectedChildCount < childCount) {
-            for (int i = expectedChildCount; i < childCount; i++) {
-                row.getChildAt(i).setVisibility(View.GONE);
-            }
-        }
-
-        // Bind the views in the application info section.
-        holder.title.applyFromItemInfoWithIcon(entry.pkgItem);
-
-        // Bind the view in the widget horizontal tray region.
-        for (int i = 0; i < infoList.size(); i++) {
-            WidgetCell widget = (WidgetCell) row.getChildAt(2 * i);
-            widget.applyFromCellItem(infoList.get(i), mWidgetPreviewLoader);
-            widget.setApplyBitmapDeferred(mApplyBitmapDeferred);
-            widget.ensurePreview();
-            widget.setVisibility(View.VISIBLE);
-
-            if (i > 0) {
-                row.getChildAt(2 * i - 1).setVisibility(View.VISIBLE);
-            }
-        }
-    }
-
-    @Override
-    public WidgetsRowViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-        if (DEBUG) {
-            Log.v(TAG, "\nonCreateViewHolder");
-        }
-
-        ViewGroup container = (ViewGroup) mLayoutInflater.inflate(
-                R.layout.widgets_list_row_view, parent, false);
-
-        // if the end padding is 0, then container view (horizontal scroll view) doesn't respect
-        // the end of the linear layout width + the start padding and doesn't allow scrolling.
-        container.findViewById(R.id.widgets_cell_list).setPaddingRelative(mIndent, 0, 1, 0);
-
-        return new WidgetsRowViewHolder(container);
-    }
-
-    @Override
-    public void onViewRecycled(WidgetsRowViewHolder holder) {
-        int total = holder.cellContainer.getChildCount();
-        for (int i = 0; i < total; i += 2) {
-            WidgetCell widget = (WidgetCell) holder.cellContainer.getChildAt(i);
-            widget.clear();
-        }
-    }
-
-    public boolean onFailedToRecycleView(WidgetsRowViewHolder holder) {
-        // If child views are animating, then the RecyclerView may choose not to recycle the view,
-        // causing extraneous onCreateViewHolder() calls.  It is safe in this case to continue
-        // recycling this view, and take care in onViewRecycled() to cancel any existing
-        // animations.
-        return true;
-    }
-
-    @Override
-    public long getItemId(int pos) {
-        return pos;
-    }
-
-    /**
-     * Comparator for sorting WidgetListRowEntry based on package title
-     */
-    public static class WidgetListRowEntryComparator implements Comparator<WidgetListRowEntry> {
-
-        private final LabelComparator mComparator = new LabelComparator();
-
-        @Override
-        public int compare(WidgetListRowEntry a, WidgetListRowEntry b) {
-            return mComparator.compare(a.pkgItem.title.toString(), b.pkgItem.title.toString());
-        }
-    }
-}
diff --git a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
index 1086987..8b3bbce 100644
--- a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
@@ -22,8 +22,9 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 /**
  * Custom app widget provider info that can be used as a widget, but provide extra functionality
@@ -57,7 +58,7 @@
     }
 
     @Override
-    public void initSpans(Context context) { }
+    public void initSpans(Context context, InvariantDeviceProfile idp) { }
 
     @Override
     public String getLabel(PackageManager packageManager) {
diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
index 0b66ec0..329a444 100644
--- a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
+++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
@@ -16,7 +16,7 @@
 
 package com.android.launcher3.widget.custom;
 
-import static com.android.launcher3.LauncherAppWidgetProviderInfo.CLS_CUSTOM_WIDGET_PREFIX;
+import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.CLS_CUSTOM_WIDGET_PREFIX;
 
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
@@ -29,12 +29,12 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.systemui.plugins.CustomWidgetPlugin;
 import com.android.systemui.plugins.PluginListener;
 
diff --git a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
new file mode 100644
index 0000000..09517e1
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2021 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.widget.model;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.PackageItemInfo;
+
+import java.lang.annotation.Retention;
+
+/** Holder class to store the package information of an entry shown in the widgets list. */
+public abstract class WidgetsListBaseEntry {
+    public final PackageItemInfo mPkgItem;
+
+    /**
+     * Character that is used as a section name for the {@link ItemInfo#title}.
+     * (e.g., "G" will be stored if title is "Google")
+     */
+    public final String mTitleSectionName;
+
+    public WidgetsListBaseEntry(PackageItemInfo pkgItem, String titleSectionName) {
+        mPkgItem = pkgItem;
+        mTitleSectionName = titleSectionName;
+    }
+
+    /**
+     * Returns the ranking of this entry in the
+     * {@link com.android.launcher3.widget.picker.WidgetsListAdapter}.
+     *
+     * <p>Entries with smaller value should be shown first. See
+     * {@link com.android.launcher3.widget.picker.WidgetsDiffReporter} for more details.
+     */
+    @Rank
+    public abstract int getRank();
+
+    @Retention(SOURCE)
+    @IntDef({RANK_WIDGETS_LIST_HEADER, RANK_WIDGETS_LIST_CONTENT})
+    public @interface Rank {
+    }
+
+    public static final int RANK_WIDGETS_LIST_HEADER = 1;
+    public static final int RANK_WIDGETS_LIST_CONTENT = 2;
+}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
new file mode 100644
index 0000000..b0cb8c7
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java
@@ -0,0 +1,50 @@
+/*
+ * 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.widget.model;
+
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.WidgetItemComparator;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Holder class to store all the information related to a list of widgets from the same app which is
+ * shown in the {@link com.android.launcher3.widget.picker.WidgetsFullSheet}.
+ */
+public final class WidgetsListContentEntry extends WidgetsListBaseEntry {
+
+    public final List<WidgetItem> mWidgets;
+
+    public WidgetsListContentEntry(PackageItemInfo pkgItem, String titleSectionName,
+            List<WidgetItem> items) {
+        super(pkgItem, titleSectionName);
+        this.mWidgets =
+                items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList());
+    }
+
+    @Override
+    public String toString() {
+        return mPkgItem.packageName + ":" + mWidgets.size();
+    }
+
+    @Override
+    @Rank
+    public int getRank() {
+        return RANK_WIDGETS_LIST_CONTENT;
+    }
+}
diff --git a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
new file mode 100644
index 0000000..6899647
--- /dev/null
+++ b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 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.widget.model;
+
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.model.data.PackageItemInfo;
+
+import java.util.Collection;
+
+/** An information holder for an app which has widgets or/and shortcuts. */
+public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry {
+
+    public final int widgetsCount;
+    public final int shortcutsCount;
+
+    private boolean mIsWidgetListShown = false;
+    private boolean mHasEntryUpdated = false;
+
+    public WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName,
+            Collection<WidgetItem> items) {
+        super(pkgItem, titleSectionName);
+        widgetsCount = (int) items.stream().filter(item -> item.widgetInfo != null).count();
+        shortcutsCount = Math.max(0, items.size() - widgetsCount);
+    }
+
+    /** Sets if the widgets list associated with this header is shown. */
+    public void setIsWidgetListShown(boolean isWidgetListShown) {
+        if (mIsWidgetListShown != isWidgetListShown) {
+            this.mIsWidgetListShown = isWidgetListShown;
+            mHasEntryUpdated = true;
+        } else {
+            mHasEntryUpdated = false;
+        }
+    }
+
+    /** Returns {@code true} if the widgets list associated with this header is shown. */
+    public boolean isWidgetListShown() {
+        return mIsWidgetListShown;
+    }
+
+    /** Returns {@code true} if this entry has been updated due to user interactions. */
+    public boolean hasEntryUpdated() {
+        return mHasEntryUpdated;
+    }
+
+    @Override
+    @Rank
+    public int getRank() {
+        return RANK_WIDGETS_LIST_HEADER;
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
new file mode 100644
index 0000000..95fa05f
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import android.view.View;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.widget.RelativeLayout;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.views.RecyclerViewFastScroller;
+import com.android.launcher3.widget.picker.WidgetsFullSheet.SearchAndRecommendationViewHolder;
+import com.android.launcher3.workprofile.PersonalWorkPagedView;
+
+/**
+ * A controller which measures & updates {@link WidgetsFullSheet}'s views padding, margin and
+ * vertical displacement upon scrolling.
+ */
+final class SearchAndRecommendationsScrollController implements
+        RecyclerViewFastScroller.OnFastScrollChangeListener {
+    private final boolean mHasWorkProfile;
+    private final SearchAndRecommendationViewHolder mViewHolder;
+    private final WidgetsRecyclerView mPrimaryRecyclerView;
+
+    // The following are only non null if mHasWorkProfile is true.
+    @Nullable private final WidgetsRecyclerView mWorkRecyclerView;
+    @Nullable private final View mPrimaryWorkTabsView;
+    @Nullable private final PersonalWorkPagedView mPrimaryWorkViewPager;
+
+    private WidgetsRecyclerView mCurrentRecyclerView;
+    private int mMaxCollapsibleHeight = 0;
+
+    SearchAndRecommendationsScrollController(
+            boolean hasWorkProfile,
+            SearchAndRecommendationViewHolder viewHolder,
+            WidgetsRecyclerView primaryRecyclerView,
+            @Nullable WidgetsRecyclerView workRecyclerView,
+            @Nullable View personalWorkTabsView,
+            @Nullable PersonalWorkPagedView primaryWorkViewPager) {
+        mHasWorkProfile = hasWorkProfile;
+        mViewHolder = viewHolder;
+        mPrimaryRecyclerView = primaryRecyclerView;
+        mWorkRecyclerView = workRecyclerView;
+        mPrimaryWorkTabsView = personalWorkTabsView;
+        mPrimaryWorkViewPager = primaryWorkViewPager;
+        mCurrentRecyclerView = mPrimaryRecyclerView;
+    }
+
+    /** Sets the current active {@link WidgetsRecyclerView}. */
+    public void setCurrentRecyclerView(WidgetsRecyclerView currentRecyclerView) {
+        mCurrentRecyclerView = currentRecyclerView;
+    }
+
+    /**
+     * Updates the margin and padding of {@link WidgetsFullSheet} to accumulate collapsible views.
+     */
+    public void updateMarginAndPadding() {
+        // The maximum vertical distance, in pixels, until the last collapsible element is not
+        // visible from the screen when the user scrolls down the recycler view.
+        mMaxCollapsibleHeight = mViewHolder.mContainer.getPaddingTop()
+                + measureHeightWithVerticalMargins(mViewHolder.mCollapseHandle)
+                + measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle);
+
+        int topContainerHeight = measureHeightWithVerticalMargins(mViewHolder.mContainer);
+        if (mHasWorkProfile) {
+            // In a work profile setup, the full widget sheet contains the following views:
+            //           -------               -|
+            //           Widgets               -|---> LinearLayout for search & recommendations
+            //          Search bar             -|
+            //      Personal | Work
+            //           View Pager
+            //
+            // Views after the search & recommendations are not bound by RelativelyLayout param.
+            // To position them on the expected location, padding & margin are added to these views
+
+            // Tabs should have a padding of the height of the search & recommendations container.
+            mPrimaryWorkTabsView.setPadding(
+                    mPrimaryWorkTabsView.getPaddingLeft(),
+                    topContainerHeight,
+                    mPrimaryWorkTabsView.getPaddingRight(),
+                    mPrimaryWorkTabsView.getPaddingBottom());
+
+            // Instead of setting the top offset directly, we split the top offset into two values:
+            // 1. topOffsetAfterAllViewsCollapsed: this is the top offset after all collapsible
+            //    views are no longer visible on the screen.
+            //    This value is set as the margin for the view pager.
+            // 2. mMaxCollapsibleDistance
+            //    This value is set as the padding for the recycler views in order to work with
+            //    clipToPadding="false", which is an attribute for not showing top / bottom padding
+            //    when a recycler view has not reached the top or bottom of the list.
+            //    e.g. a list of 10 entries, only 3 entries are visible at a time.
+            //         case 1: recycler view is scrolled to the top. Top padding is visible/
+            //         (top padding)
+            //         item 1
+            //         item 2
+            //         item 3
+            //
+            //         case 2: recycler view is scrolled to the middle. No padding is visible.
+            //         item 4
+            //         item 5
+            //         item 6
+            //
+            //         case 3: recycler view is scrolled to the end. bottom padding is visible.
+            //         item 8
+            //         item 9
+            //         item 10
+            //         (bottom padding): not set in this case.
+            //
+            // When the views are first inflated, the sum of topOffsetAfterAllViewsCollapsed and
+            // mMaxCollapsibleDistance should equal to the top container height.
+            int tabsViewActualHeight = measureHeightWithVerticalMargins(mPrimaryWorkTabsView)
+                    - mPrimaryWorkTabsView.getPaddingTop();
+            int topOffsetAfterAllViewsCollapsed =
+                    topContainerHeight + tabsViewActualHeight - mMaxCollapsibleHeight;
+
+            RelativeLayout.LayoutParams layoutParams =
+                    (RelativeLayout.LayoutParams) mPrimaryWorkViewPager.getLayoutParams();
+            layoutParams.setMargins(0, topOffsetAfterAllViewsCollapsed, 0, 0);
+            mPrimaryWorkViewPager.setLayoutParams(layoutParams);
+            mPrimaryWorkViewPager.requestLayout();
+
+            mPrimaryRecyclerView.setPadding(
+                    mPrimaryRecyclerView.getPaddingLeft(),
+                    mMaxCollapsibleHeight,
+                    mPrimaryRecyclerView.getPaddingRight(),
+                    mPrimaryRecyclerView.getPaddingBottom());
+            mWorkRecyclerView.setPadding(
+                    mWorkRecyclerView.getPaddingLeft(),
+                    mMaxCollapsibleHeight,
+                    mWorkRecyclerView.getPaddingRight(),
+                    mWorkRecyclerView.getPaddingBottom());
+        } else {
+            mPrimaryRecyclerView.setPadding(
+                    mPrimaryRecyclerView.getPaddingLeft(),
+                    topContainerHeight,
+                    mPrimaryRecyclerView.getPaddingRight(),
+                    mPrimaryRecyclerView.getPaddingBottom());
+        }
+    }
+
+    /**
+     * Changes the displacement of collapsible views (e.g. title & widget recommendations) and fixed
+     * views (e.g. recycler views, tabs) upon scrolling.
+     */
+    @Override
+    public void onThumbOffsetYChanged(int unused) {
+        // Always use the recycler view offset because fast scroller offset has a different scale.
+        int recyclerViewYOffset = mCurrentRecyclerView.getCurrentScrollY();
+        if (recyclerViewYOffset < 0) return;
+        if (mMaxCollapsibleHeight > 0) {
+            int yDisplacement = Math.max(-recyclerViewYOffset, -mMaxCollapsibleHeight);
+            mViewHolder.mHeaderTitle.setTranslationY(yDisplacement);
+            mViewHolder.mSearchBar.setTranslationY(yDisplacement);
+            if (mHasWorkProfile) {
+                mPrimaryWorkTabsView.setTranslationY(yDisplacement);
+            }
+        }
+    }
+
+    /** Resets any previous view translation. */
+    public void reset() {
+        mViewHolder.mHeaderTitle.setTranslationY(0);
+        mViewHolder.mSearchBar.setTranslationY(0);
+        if (mHasWorkProfile) {
+            mPrimaryWorkTabsView.setTranslationY(0);
+        }
+    }
+
+    /** private the height, in pixel, + the vertical margins of a given view. */
+    private static int measureHeightWithVerticalMargins(View view) {
+        MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
+        return view.getMeasuredHeight() + marginLayoutParams.bottomMargin
+                + marginLayoutParams.topMargin;
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
new file mode 100644
index 0000000..dbd1bdf
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker;
+
+import android.util.Log;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.picker.WidgetsListAdapter.WidgetListBaseRowEntryComparator;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Do diff on widget's tray list items and call the {@link RecyclerView.Adapter}
+ * methods accordingly.
+ */
+public class WidgetsDiffReporter {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "WidgetsDiffReporter";
+
+    private final IconCache mIconCache;
+    private final RecyclerView.Adapter mListener;
+
+    public WidgetsDiffReporter(IconCache iconCache, RecyclerView.Adapter listener) {
+        mIconCache = iconCache;
+        mListener = listener;
+    }
+
+    /**
+     * Notifies the difference between {@code currentEntries} & {@code newEntries} by calling the
+     * relevant {@link androidx.recyclerview.widget.RecyclerView.RecyclerViewDataObserver} methods.
+     */
+    public void process(ArrayList<WidgetsListBaseEntry> currentEntries,
+            List<WidgetsListBaseEntry> newEntries,
+            WidgetListBaseRowEntryComparator comparator) {
+        if (DEBUG) {
+            Log.d(TAG, "process oldEntries#=" + currentEntries.size()
+                    + " newEntries#=" + newEntries.size());
+        }
+        // Early exit if either of the list is empty
+        if (currentEntries.isEmpty() || newEntries.isEmpty()) {
+            // Skip if both list are empty.
+            // On rotation, we open the widget tray with empty. Then try to fetch the list again
+            // when the animation completes (which still gives empty). And we get the final result
+            // when the bind actually completes.
+            if (currentEntries.size() != newEntries.size()) {
+                currentEntries.clear();
+                currentEntries.addAll(newEntries);
+                mListener.notifyDataSetChanged();
+            }
+            return;
+        }
+        ArrayList<WidgetsListBaseEntry> orgEntries =
+                (ArrayList<WidgetsListBaseEntry>) currentEntries.clone();
+        Iterator<WidgetsListBaseEntry> orgIter = orgEntries.iterator();
+        Iterator<WidgetsListBaseEntry> newIter = newEntries.iterator();
+
+        WidgetsListBaseEntry orgRowEntry = orgIter.next();
+        WidgetsListBaseEntry newRowEntry = newIter.next();
+
+        do {
+            int diff = compareAppNameAndType(orgRowEntry, newRowEntry, comparator);
+            if (DEBUG) {
+                Log.d(TAG, String.format("diff=%d orgRowEntry (%s) newRowEntry (%s)",
+                        diff, orgRowEntry != null ? orgRowEntry.toString() : null,
+                        newRowEntry != null ? newRowEntry.toString() : null));
+            }
+            int index = -1;
+            if (diff < 0) {
+                index = currentEntries.indexOf(orgRowEntry);
+                mListener.notifyItemRemoved(index);
+                if (DEBUG) {
+                    Log.d(TAG, String.format("notifyItemRemoved called (%d)%s", index,
+                            orgRowEntry.mTitleSectionName));
+                }
+                currentEntries.remove(index);
+                orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
+            } else if (diff > 0) {
+                index = orgRowEntry != null ? currentEntries.indexOf(orgRowEntry)
+                        : currentEntries.size();
+                currentEntries.add(index, newRowEntry);
+                if (DEBUG) {
+                    Log.d(TAG, String.format("notifyItemInserted called (%d)%s", index,
+                            newRowEntry.mTitleSectionName));
+                }
+                newRowEntry = newIter.hasNext() ? newIter.next() : null;
+                mListener.notifyItemInserted(index);
+
+            } else {
+                // same app name & type but,
+                // did the icon, title, etc, change?
+                // or did the header view changed due to user interactions?
+                // or did the widget size and desc, span, etc change?
+                if (!isSamePackageItemInfo(orgRowEntry.mPkgItem, newRowEntry.mPkgItem)
+                        || hasHeaderUpdated(newRowEntry)
+                        || hasWidgetsListChanged(orgRowEntry, newRowEntry)) {
+                    index = currentEntries.indexOf(orgRowEntry);
+                    currentEntries.set(index, newRowEntry);
+                    mListener.notifyItemChanged(index);
+                    if (DEBUG) {
+                        Log.d(TAG, String.format("notifyItemChanged called (%d)%s", index,
+                                newRowEntry.mTitleSectionName));
+                    }
+                }
+                orgRowEntry = orgIter.hasNext() ? orgIter.next() : null;
+                newRowEntry = newIter.hasNext() ? newIter.next() : null;
+            }
+        } while(orgRowEntry != null || newRowEntry != null);
+    }
+
+    /**
+     * Compares the app name and then entry type for the given {@link WidgetsListBaseEntry}s.
+     *
+     * @Return 0 if both entries' order is the same. Negative integer if {@code newRowEntry} should
+     *         order before {@code orgRowEntry}. Positive integer if {@code orgRowEntry} should
+     *         order before {@code newRowEntry}.
+     */
+    private int compareAppNameAndType(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow,
+            WidgetListBaseRowEntryComparator comparator) {
+        if (curRow == null && newRow == null) {
+            throw new IllegalStateException(
+                    "Cannot compare PackageItemInfo if both rows are null.");
+        }
+
+        if (curRow == null && newRow != null) {
+            return 1; // new row needs to be inserted
+        } else if (curRow != null && newRow == null) {
+            return -1; // old row needs to be deleted
+        }
+        int diff = comparator.compare(curRow, newRow);
+        if (diff == 0) {
+            return newRow.getRank() - curRow.getRank();
+        }
+        return diff;
+    }
+
+    /**
+     * Returns {@code true} if both {@code curRow} & {@code newRow} are
+     * {@link WidgetsListContentEntry}s with a different list of widgets.
+     */
+    private boolean hasWidgetsListChanged(WidgetsListBaseEntry curRow,
+            WidgetsListBaseEntry newRow) {
+        if (!(curRow instanceof WidgetsListContentEntry)
+                || !(newRow instanceof WidgetsListContentEntry)) {
+            return false;
+        }
+        WidgetsListContentEntry orgRowEntry = (WidgetsListContentEntry) curRow;
+        WidgetsListContentEntry newRowEntry = (WidgetsListContentEntry) newRow;
+        return !orgRowEntry.mWidgets.equals(newRowEntry.mWidgets);
+    }
+
+    /**
+     * Returns {@code true} if {@code newRow} is {@link WidgetsListHeaderEntry} and its content has
+     * been changed due to user interactions.
+     */
+    private boolean hasHeaderUpdated(WidgetsListBaseEntry newRow) {
+        if (!(newRow instanceof WidgetsListHeaderEntry)) {
+            return false;
+        }
+        WidgetsListHeaderEntry newRowEntry = (WidgetsListHeaderEntry) newRow;
+        return newRowEntry.hasEntryUpdated();
+    }
+
+    private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) {
+        return curInfo.bitmap.icon.equals(newInfo.bitmap.icon)
+                && !mIconCache.isDefaultIcon(curInfo.bitmap, curInfo.user);
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
new file mode 100644
index 0000000..bf9b849
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.content.pm.LauncherApps;
+import android.graphics.Rect;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.AttributeSet;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.views.RecyclerViewFastScroller;
+import com.android.launcher3.views.TopRoundedCornerView;
+import com.android.launcher3.widget.BaseWidgetSheet;
+import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListener;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.workprofile.PersonalWorkPagedView;
+import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
+
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * Popup for showing the full list of available widgets
+ */
+public class WidgetsFullSheet extends BaseWidgetSheet
+        implements Insettable, ProviderChangedListener, OnActivePageChangedListener,
+        WidgetsRecyclerView.HeaderViewDimensionsProvider {
+
+    private static final long DEFAULT_OPEN_DURATION = 267;
+    private static final long FADE_IN_DURATION = 150;
+    private static final float VERTICAL_START_POSITION = 0.3f;
+
+    private final Rect mInsets = new Rect();
+    private final boolean mHasWorkProfile;
+    private final SparseArray<AdapterHolder> mAdapters = new SparseArray();
+    private final UserHandle mCurrentUser = Process.myUserHandle();
+    private final Predicate<WidgetsListBaseEntry> mPrimaryWidgetsFilter = entry ->
+            mCurrentUser.equals(entry.mPkgItem.user);
+    private final Predicate<WidgetsListBaseEntry> mWorkWidgetsFilter =
+            mPrimaryWidgetsFilter.negate();
+
+    @Nullable private PersonalWorkPagedView mViewPager;
+    private int mInitialTabsHeight = 0;
+    private View mTabsView;
+    private SearchAndRecommendationViewHolder mSearchAndRecommendationViewHolder;
+    private SearchAndRecommendationsScrollController mSearchAndRecommendationsScrollController;
+
+    public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mHasWorkProfile = context.getSystemService(LauncherApps.class).getProfiles().size() > 1;
+        mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY));
+        mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK));
+    }
+
+    public WidgetsFullSheet(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mContent = findViewById(R.id.container);
+        TopRoundedCornerView springLayout = (TopRoundedCornerView) mContent;
+
+        LayoutInflater layoutInflater = LayoutInflater.from(getContext());
+        int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view
+                : R.layout.widgets_full_sheet_recyclerview;
+        layoutInflater.inflate(contentLayoutRes, springLayout, true);
+
+        RecyclerViewFastScroller fastScroller = findViewById(R.id.fast_scroller);
+        if (mHasWorkProfile) {
+            mViewPager = findViewById(R.id.widgets_view_pager);
+            mViewPager.initParentViews(this);
+            mViewPager.getPageIndicator().setOnActivePageChangedListener(this);
+            mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.PRIMARY);
+            mTabsView = findViewById(R.id.tabs);
+            findViewById(R.id.tab_personal)
+                    .setOnClickListener((View view) -> mViewPager.snapToPage(0));
+            findViewById(R.id.tab_work)
+                    .setOnClickListener((View view) -> mViewPager.snapToPage(1));
+            fastScroller.setIsRecyclerViewFirstChildInParent(false);
+            springLayout.addSpringView(R.id.primary_widgets_list_view);
+            springLayout.addSpringView(R.id.work_widgets_list_view);
+        } else {
+            mViewPager = null;
+            springLayout.addSpringView(R.id.primary_widgets_list_view);
+        }
+
+        layoutInflater.inflate(R.layout.widgets_full_sheet_search_and_recommendations, springLayout,
+                true);
+        springLayout.addSpringView(R.id.search_and_recommendations_container);
+
+        mSearchAndRecommendationViewHolder = new SearchAndRecommendationViewHolder(
+                findViewById(R.id.search_and_recommendations_container));
+        mSearchAndRecommendationsScrollController = new SearchAndRecommendationsScrollController(
+                mHasWorkProfile,
+                mSearchAndRecommendationViewHolder,
+                findViewById(R.id.primary_widgets_list_view),
+                mHasWorkProfile ? findViewById(R.id.work_widgets_list_view) : null,
+                mTabsView,
+                mViewPager);
+        fastScroller.setOnFastScrollChangeListener(mSearchAndRecommendationsScrollController);
+
+        onWidgetsBound();
+    }
+
+    @Override
+    public void onActivePageChanged(int currentActivePage) {
+        WidgetsRecyclerView currentRecyclerView =
+                mAdapters.get(currentActivePage).mWidgetsRecyclerView;
+        currentRecyclerView.bindFastScrollbar();
+        mSearchAndRecommendationsScrollController.setCurrentRecyclerView(currentRecyclerView);
+
+        reset();
+    }
+
+    private void reset() {
+        mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop();
+        if (mHasWorkProfile) {
+            mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop();
+        }
+        mSearchAndRecommendationsScrollController.reset();
+    }
+
+    @VisibleForTesting
+    public WidgetsRecyclerView getRecyclerView() {
+        if (!mHasWorkProfile || mViewPager.getCurrentPage() == AdapterHolder.PRIMARY) {
+            return mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView;
+        }
+        return mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView;
+    }
+
+    @Override
+    protected Pair<View, String> getAccessibilityTarget() {
+        return Pair.create(getRecyclerView(), getContext().getString(
+                mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed));
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mLauncher.getAppWidgetHost().addProviderChangeListener(this);
+        notifyWidgetProvidersChanged();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mLauncher.getAppWidgetHost().removeProviderChangeListener(this);
+    }
+
+    @Override
+    public void setInsets(Rect insets) {
+        mInsets.set(insets);
+
+        setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, insets.bottom);
+        if (mHasWorkProfile) {
+            setBottomPadding(mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView, insets.bottom);
+        }
+        if (insets.bottom > 0) {
+            setupNavBarColor();
+        } else {
+            clearNavBarColor();
+        }
+
+        ((TopRoundedCornerView) mContent).setNavBarScrimHeight(mInsets.bottom);
+        requestLayout();
+    }
+
+    private void setBottomPadding(RecyclerView recyclerView, int bottomPadding) {
+        recyclerView.setPadding(
+                recyclerView.getPaddingLeft(),
+                recyclerView.getPaddingTop(),
+                recyclerView.getPaddingRight(),
+                bottomPadding);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        DeviceProfile deviceProfile = mLauncher.getDeviceProfile();
+        int widthUsed;
+        if (mInsets.bottom > 0) {
+            widthUsed = mInsets.left + mInsets.right;
+        } else {
+            Rect padding = deviceProfile.workspacePadding;
+            widthUsed = Math.max(padding.left + padding.right,
+                    2 * (mInsets.left + mInsets.right));
+        }
+
+        int heightUsed = mInsets.top + deviceProfile.edgeMarginPx;
+        measureChildWithMargins(mContent, widthMeasureSpec,
+                widthUsed, heightMeasureSpec, heightUsed);
+        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
+                MeasureSpec.getSize(heightMeasureSpec));
+
+        int paddingPx = 2 * getResources().getDimensionPixelOffset(
+                R.dimen.widget_cell_horizontal_padding);
+        int maxSpansPerRow = getMeasuredWidth() / (deviceProfile.cellWidthPx + paddingPx);
+        mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
+                maxSpansPerRow);
+        if (mHasWorkProfile) {
+            mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
+                    maxSpansPerRow);
+        }
+
+        if (mInitialTabsHeight == 0 && mTabsView != null) {
+            mInitialTabsHeight = measureHeightWithVerticalMargins(mTabsView);
+        }
+
+        mSearchAndRecommendationsScrollController.updateMarginAndPadding();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        int width = r - l;
+        int height = b - t;
+
+        // Content is laid out as center bottom aligned
+        int contentWidth = mContent.getMeasuredWidth();
+        int contentLeft = (width - contentWidth - mInsets.left - mInsets.right) / 2 + mInsets.left;
+        mContent.layout(contentLeft, height - mContent.getMeasuredHeight(),
+                contentLeft + contentWidth, height);
+
+        setTranslationShift(mTranslationShift);
+    }
+
+    @Override
+    public void notifyWidgetProvidersChanged() {
+        mLauncher.refreshAndBindWidgetsForPackageUser(null);
+    }
+
+    @Override
+    public void onWidgetsBound() {
+        List<WidgetsListBaseEntry> allWidgets = mLauncher.getPopupDataProvider().getAllWidgets();
+
+        AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY);
+        primaryUserAdapterHolder.setup(findViewById(R.id.primary_widgets_list_view));
+        primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
+        if (mHasWorkProfile) {
+            AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK);
+            workUserAdapterHolder.setup(findViewById(R.id.work_widgets_list_view));
+            workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
+            onActivePageChanged(mViewPager.getCurrentPage());
+        }
+    }
+
+    private void open(boolean animate) {
+        if (animate) {
+            if (getPopupContainer().getInsets().bottom > 0) {
+                mContent.setAlpha(0);
+                setTranslationShift(VERTICAL_START_POSITION);
+            }
+            mOpenCloseAnimator.setValues(
+                    PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
+            mOpenCloseAnimator
+                    .setDuration(DEFAULT_OPEN_DURATION)
+                    .setInterpolator(AnimationUtils.loadInterpolator(
+                            getContext(), android.R.interpolator.linear_out_slow_in));
+            mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mOpenCloseAnimator.removeListener(this);
+                }
+            });
+            post(() -> {
+                mOpenCloseAnimator.start();
+                mContent.animate().alpha(1).setDuration(FADE_IN_DURATION);
+            });
+        } else {
+            setTranslationShift(TRANSLATION_SHIFT_OPENED);
+            post(this::announceAccessibilityChanges);
+        }
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        handleClose(animate, DEFAULT_OPEN_DURATION);
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_WIDGETS_FULL_SHEET) != 0;
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        // Disable swipe down when recycler view is scrolling
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            mNoIntercept = false;
+            RecyclerViewFastScroller scroller = getRecyclerView().getScrollbar();
+            if (scroller.getThumbOffsetY() >= 0
+                    && getPopupContainer().isEventOverView(scroller, ev)) {
+                mNoIntercept = true;
+            } else if (getPopupContainer().isEventOverView(mContent, ev)) {
+                mNoIntercept = !getRecyclerView().shouldContainerScroll(ev, getPopupContainer());
+            }
+        }
+        return super.onControllerInterceptTouchEvent(ev);
+    }
+
+    /** Shows the {@link WidgetsFullSheet} on the launcher. */
+    public static WidgetsFullSheet show(Launcher launcher, boolean animate) {
+        WidgetsFullSheet sheet = (WidgetsFullSheet) launcher.getLayoutInflater()
+                .inflate(R.layout.widgets_full_sheet, launcher.getDragLayer(), false);
+        sheet.attachToContainer();
+        sheet.mIsOpen = true;
+        sheet.open(animate);
+        return sheet;
+    }
+
+    /** Gets the {@link WidgetsRecyclerView} which shows all widgets in {@link WidgetsFullSheet}. */
+    @VisibleForTesting
+    public static WidgetsRecyclerView getWidgetsView(Launcher launcher) {
+        return launcher.findViewById(R.id.primary_widgets_list_view);
+    }
+
+    @Override
+    public void addHintCloseAnim(
+            float distanceToMove, Interpolator interpolator, PendingAnimation target) {
+        target.setFloat(getRecyclerView(), VIEW_TRANSLATE_Y, -distanceToMove, interpolator);
+        target.setViewAlpha(getRecyclerView(), 0.5f, interpolator);
+    }
+
+    @Override
+    protected void onCloseComplete() {
+        super.onCloseComplete();
+        AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL);
+    }
+
+    @Override
+    public int getHeaderViewHeight() {
+        // No need to check work profile here because mInitialTabHeight is always 0 if there is no
+        // work profile.
+        return mInitialTabsHeight
+                + measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mContainer);
+    }
+
+    /** private the height, in pixel, + the vertical margins of a given view. */
+    private static int measureHeightWithVerticalMargins(View view) {
+        MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
+        return view.getMeasuredHeight() + marginLayoutParams.bottomMargin
+                + marginLayoutParams.topMargin;
+    }
+
+    /** A holder class for holding adapters & their corresponding recycler view. */
+    private final class AdapterHolder {
+        static final int PRIMARY = 0;
+        static final int WORK = 1;
+
+        private final int mAdapterType;
+        private final WidgetsListAdapter mWidgetsListAdapter;
+
+        private WidgetsRecyclerView mWidgetsRecyclerView;
+
+        AdapterHolder(int adapterType) {
+            mAdapterType = adapterType;
+
+            Context context = getContext();
+            LauncherAppState apps = LauncherAppState.getInstance(context);
+            mWidgetsListAdapter = new WidgetsListAdapter(
+                    context,
+                    LayoutInflater.from(context),
+                    apps.getWidgetCache(),
+                    apps.getIconCache(),
+                    /* iconClickListener= */ WidgetsFullSheet.this,
+                    /* iconLongClickListener= */ WidgetsFullSheet.this);
+            mWidgetsListAdapter.setFilter(
+                    mAdapterType == PRIMARY ? mPrimaryWidgetsFilter : mWorkWidgetsFilter);
+        }
+
+        void setup(WidgetsRecyclerView recyclerView) {
+            mWidgetsRecyclerView = recyclerView;
+            mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter);
+            mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this);
+            mWidgetsRecyclerView.setEdgeEffectFactory(
+                    ((TopRoundedCornerView) mContent).createEdgeEffectFactory());
+            mWidgetsListAdapter.setApplyBitmapDeferred(false, mWidgetsRecyclerView);
+        }
+    }
+
+    final class SearchAndRecommendationViewHolder {
+        final View mContainer;
+        final View mCollapseHandle;
+        final EditText mSearchBar;
+        final TextView mHeaderTitle;
+
+        SearchAndRecommendationViewHolder(View searchAndRecommendationContainer) {
+            mContainer = searchAndRecommendationContainer;
+            mCollapseHandle = mContainer.findViewById(R.id.collapse_handle);
+            mSearchBar = mContainer.findViewById(R.id.widgets_search_bar);
+            mHeaderTitle = mContainer.findViewById(R.id.title);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
new file mode 100644
index 0000000..8b49d1e
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.widget.picker;
+
+import android.content.Context;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+import android.widget.TableRow;
+
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.Adapter;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+import com.android.launcher3.R;
+import com.android.launcher3.WidgetPreviewLoader;
+import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.util.LabelComparator;
+import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.picker.WidgetsListHeaderViewHolderBinder.OnHeaderClickListener;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * Recycler view adapter for the widget tray.
+ *
+ * <p>This adapter supports view binding of subclasses of {@link WidgetsListBaseEntry}. There are 2
+ * subclasses: {@link WidgetsListHeader} & {@link WidgetsListContentEntry}.
+ * {@link WidgetsListHeader} entries are always visible in the recycler view. At most one
+ * {@link WidgetsListContentEntry} is shown in the recycler view at any time. Clicking a
+ * {@link WidgetsListHeader} will result in expanding / collapsing a corresponding
+ * {@link WidgetsListContentEntry} of the same app.
+ */
+public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderClickListener {
+
+    private static final String TAG = "WidgetsListAdapter";
+    private static final boolean DEBUG = false;
+
+    /** Uniquely identifies widgets list view type within the app. */
+    private static final int VIEW_TYPE_WIDGETS_LIST = R.layout.widgets_list_row_view;
+    private static final int VIEW_TYPE_WIDGETS_HEADER = R.layout.widgets_list_row_header;
+
+    private final WidgetsDiffReporter mDiffReporter;
+    private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
+    private final WidgetsListTableViewHolderBinder mWidgetsListTableViewHolderBinder;
+    private final WidgetListBaseRowEntryComparator mRowComparator =
+            new WidgetListBaseRowEntryComparator();
+
+    private List<WidgetsListBaseEntry> mAllEntries = new ArrayList<>();
+    private ArrayList<WidgetsListBaseEntry> mVisibleEntries = new ArrayList<>();
+    @Nullable private String mWidgetsContentVisiblePackage = null;
+
+    private Predicate<WidgetsListBaseEntry> mHeaderAndSelectedContentFilter = entry ->
+            entry instanceof WidgetsListHeaderEntry
+                    || entry.mPkgItem.packageName.equals(mWidgetsContentVisiblePackage);
+    @Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
+
+    public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
+            WidgetPreviewLoader widgetPreviewLoader, IconCache iconCache,
+            OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) {
+        mDiffReporter = new WidgetsDiffReporter(iconCache, this);
+        mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(context,
+                layoutInflater, iconClickListener, iconLongClickListener, widgetPreviewLoader);
+        mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListTableViewHolderBinder);
+        mViewHolderBinders.put(VIEW_TYPE_WIDGETS_HEADER,
+                new WidgetsListHeaderViewHolderBinder(layoutInflater, this::onHeaderClicked));
+    }
+
+    public void setFilter(Predicate<WidgetsListBaseEntry> filter) {
+        mFilter = filter;
+    }
+
+    /**
+     * Defers applying bitmap on all the {@link WidgetCell} in the {@param rv}.
+     *
+     * @see WidgetCell#setApplyBitmapDeferred(boolean)
+     */
+    public void setApplyBitmapDeferred(boolean isDeferred, RecyclerView rv) {
+        mWidgetsListTableViewHolderBinder.setApplyBitmapDeferred(isDeferred);
+
+        for (int i = rv.getChildCount() - 1; i >= 0; i--) {
+            ViewHolder viewHolder = rv.getChildViewHolder(rv.getChildAt(i));
+            if (viewHolder.getItemViewType() == VIEW_TYPE_WIDGETS_LIST) {
+                WidgetsRowViewHolder holder = (WidgetsRowViewHolder) viewHolder;
+                for (int j = holder.mTableContainer.getChildCount() - 1; j >= 0; j--) {
+                    TableRow row =  (TableRow) holder.mTableContainer.getChildAt(j);
+                    for (int k = row.getChildCount() - 1; k >= 0; k--) {
+                        ((WidgetCell) row.getChildAt(k)).setApplyBitmapDeferred(isDeferred);
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        return mVisibleEntries.size();
+    }
+
+    /** Gets the section name for {@link com.android.launcher3.views.RecyclerViewFastScroller}. */
+    public String getSectionName(int pos) {
+        return mVisibleEntries.get(pos).mTitleSectionName;
+    }
+
+    /** Updates the widget list. */
+    public void setWidgets(List<WidgetsListBaseEntry> tempEntries) {
+        mAllEntries = tempEntries.stream().sorted(mRowComparator)
+                .collect(Collectors.toList());
+        updateVisibleEntries();
+    }
+
+    private void updateVisibleEntries() {
+        mAllEntries.forEach(entry -> {
+            if (entry instanceof WidgetsListHeaderEntry) {
+                ((WidgetsListHeaderEntry) entry).setIsWidgetListShown(
+                        entry.mPkgItem.packageName.equals(mWidgetsContentVisiblePackage));
+            }
+        });
+        List<WidgetsListBaseEntry> newVisibleEntries = mAllEntries.stream()
+                .filter(entry -> (mFilter == null || mFilter.test(entry))
+                        && mHeaderAndSelectedContentFilter.test(entry))
+                .collect(Collectors.toList());
+        mDiffReporter.process(mVisibleEntries, newVisibleEntries, mRowComparator);
+    }
+
+    @Override
+    public void onBindViewHolder(ViewHolder holder, int pos) {
+        ViewHolderBinder viewHolderBinder = mViewHolderBinders.get(getItemViewType(pos));
+        viewHolderBinder.bindViewHolder(holder, mVisibleEntries.get(pos));
+    }
+
+    @Override
+    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        if (DEBUG) {
+            Log.v(TAG, "\nonCreateViewHolder");
+        }
+
+        return mViewHolderBinders.get(viewType).newViewHolder(parent);
+    }
+
+    @Override
+    public void onViewRecycled(ViewHolder holder) {
+        mViewHolderBinders.get(holder.getItemViewType()).unbindViewHolder(holder);
+    }
+
+    @Override
+    public boolean onFailedToRecycleView(ViewHolder holder) {
+        // If child views are animating, then the RecyclerView may choose not to recycle the view,
+        // causing extraneous onCreateViewHolder() calls.  It is safe in this case to continue
+        // recycling this view, and take care in onViewRecycled() to cancel any existing
+        // animations.
+        return true;
+    }
+
+    @Override
+    public long getItemId(int pos) {
+        return pos;
+    }
+
+    @Override
+    public int getItemViewType(int pos) {
+        WidgetsListBaseEntry entry = mVisibleEntries.get(pos);
+        if (entry instanceof WidgetsListContentEntry) {
+            return VIEW_TYPE_WIDGETS_LIST;
+        } else if (entry instanceof WidgetsListHeaderEntry) {
+            return VIEW_TYPE_WIDGETS_HEADER;
+        }
+        throw new UnsupportedOperationException("ViewHolderBinder not found for " + entry);
+    }
+
+    @Override
+    public void onHeaderClicked(boolean showWidgets, String expandedPackage) {
+        if (showWidgets) {
+            mWidgetsContentVisiblePackage = expandedPackage;
+            updateVisibleEntries();
+        } else if (expandedPackage.equals(mWidgetsContentVisiblePackage)) {
+            mWidgetsContentVisiblePackage = null;
+            updateVisibleEntries();
+        }
+    }
+
+    /**
+     * Sets the max horizontal spans that are allowed for grouping more than one widgets in a table
+     * row.
+     *
+     * <p>If there is only one widget in a row, that widget horizontal span is allowed to exceed
+     * {@code maxHorizontalSpans}.
+     * <p>Let's say the max horizontal spans is set to 5. Widgets can be grouped in the same row if
+     * their total horizontal spans added don't exceed 5.
+     * Example 1: Row 1: 2x2, 2x3, 1x1. Total horizontal spans is 5. This is okay.
+     * Example 2: Row 1: 2x2, 4x3, 1x1. the total horizontal spans is 7. This is wrong.
+     *            4x3 and 1x1 should be moved to a new row.
+     * Example 3: Row 1: 6x4. This is okay because this is the only item in the row.
+     */
+    public void setMaxHorizontalSpansPerRow(int maxHorizontalSpans) {
+        mWidgetsListTableViewHolderBinder.setMaxSpansPerRow(maxHorizontalSpans);
+    }
+
+    /** Comparator for sorting WidgetListRowEntry based on package title. */
+    public static class WidgetListBaseRowEntryComparator implements
+            Comparator<WidgetsListBaseEntry> {
+
+        private final LabelComparator mComparator = new LabelComparator();
+
+        @Override
+        public int compare(WidgetsListBaseEntry a, WidgetsListBaseEntry b) {
+            return mComparator.compare(a.mPkgItem.title.toString(), b.mPkgItem.title.toString());
+        }
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
new file mode 100644
index 0000000..070a9aa
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import static com.android.launcher3.FastBitmapDrawable.newIcon;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.FastBitmapDrawable;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.graphics.PlaceHolderIconDrawable;
+import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
+import com.android.launcher3.icons.cache.HandlerRunnable;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
+import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+
+/**
+ * A UI represents a header of an app shown in the full widgets tray.
+ *
+ * It is a {@link LinearLayout} which contains an app icon, an app name, a subtitle and a checkbox
+ * which indicates if the widgets content view underneath this header should be shown.
+ */
+public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpdateReceiver {
+
+    private boolean mEnableIconUpdateAnimation = false;
+
+    @Nullable private HandlerRunnable mIconLoadRequest;
+    @Nullable private Drawable mIconDrawable;
+    private final int mIconSize;
+
+    private ImageView mAppIcon;
+    private TextView mTitle;
+    private TextView mSubtitle;
+
+    private CheckBox mExpandToggle;
+    private boolean mIsExpanded = false;
+
+    public WidgetsListHeader(Context context) {
+        this(context, /* attrs= */ null);
+    }
+
+    public WidgetsListHeader(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, /* defStyle= */ 0);
+    }
+
+    public WidgetsListHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        ActivityContext activity = ActivityContext.lookupContext(context);
+        DeviceProfile grid = activity.getDeviceProfile();
+        TypedArray a = context.obtainStyledAttributes(attrs,
+                R.styleable.WidgetsListRowHeader, defStyleAttr, /* defStyleRes= */ 0);
+        mIconSize = a.getDimensionPixelSize(R.styleable.WidgetsListRowHeader_appIconSize,
+                grid.iconSizePx);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mAppIcon = findViewById(R.id.app_icon);
+        mTitle = findViewById(R.id.app_title);
+        mSubtitle = findViewById(R.id.app_subtitle);
+        mExpandToggle = findViewById(R.id.toggle);
+    }
+
+    /**
+     * Sets a {@link OnExpansionChangeListener} to get a callback when this app widgets section
+     * expands / collapses.
+     */
+    @UiThread
+    public void setOnExpandChangeListener(
+            @Nullable OnExpansionChangeListener onExpandChangeListener) {
+        // Use the entire touch area of this view to expand / collapse an app widgets section.
+        setOnClickListener(view -> {
+            setExpanded(!mIsExpanded);
+            onExpandChangeListener.onExpansionChange(mIsExpanded);
+        });
+    }
+
+    /** Sets the expand toggle to expand / collapse. */
+    @UiThread
+    public void setExpanded(boolean isExpanded) {
+        this.mIsExpanded = isExpanded;
+        mExpandToggle.setChecked(isExpanded);
+    }
+
+    /** Apply app icon, labels and tag using a generic {@link WidgetsListHeaderEntry}. */
+    @UiThread
+    public void applyFromItemInfoWithIcon(WidgetsListHeaderEntry entry) {
+        applyIconAndLabel(entry);
+    }
+
+    @UiThread
+    private void applyIconAndLabel(WidgetsListHeaderEntry entry) {
+        PackageItemInfo info = entry.mPkgItem;
+        setIcon(info);
+        setTitles(entry);
+        setExpanded(entry.isWidgetListShown());
+
+        super.setTag(info);
+
+        verifyHighRes();
+    }
+
+    private void setIcon(PackageItemInfo info) {
+        FastBitmapDrawable icon = newIcon(getContext(), info);
+        applyDrawables(icon);
+        mIconDrawable = icon;
+        if (mIconDrawable != null) {
+            mIconDrawable.setVisible(
+                    /* visible= */ getWindowVisibility() == VISIBLE && isShown(),
+                    /* restart= */ false);
+        }
+    }
+
+    private void applyDrawables(Drawable icon) {
+        icon.setBounds(0, 0, mIconSize, mIconSize);
+
+        mAppIcon.setImageDrawable(icon);
+
+        // If the current icon is a placeholder color, animate its update.
+        if (mIconDrawable != null
+                && mIconDrawable instanceof PlaceHolderIconDrawable
+                && mEnableIconUpdateAnimation) {
+            ((PlaceHolderIconDrawable) mIconDrawable).animateIconUpdate(icon);
+        }
+    }
+
+    private void setTitles(WidgetsListHeaderEntry entry) {
+        mTitle.setText(entry.mPkgItem.title);
+
+        Resources resources = getContext().getResources();
+        if (entry.widgetsCount == 0 && entry.shortcutsCount == 0) {
+            mSubtitle.setVisibility(GONE);
+            return;
+        }
+
+        String subtitle;
+        if (entry.widgetsCount > 0 && entry.shortcutsCount > 0) {
+            String widgetsCount = resources.getQuantityString(R.plurals.widgets_count,
+                    entry.widgetsCount, entry.widgetsCount);
+            String shortcutsCount = resources.getQuantityString(R.plurals.shortcuts_count,
+                    entry.shortcutsCount, entry.shortcutsCount);
+            subtitle = resources.getString(R.string.widgets_and_shortcuts_count, widgetsCount,
+                    shortcutsCount);
+        } else if (entry.widgetsCount > 0) {
+            subtitle = resources.getQuantityString(R.plurals.widgets_count,
+                     entry.widgetsCount, entry.widgetsCount);
+        } else {
+            subtitle = resources.getQuantityString(R.plurals.shortcuts_count,
+                    entry.shortcutsCount, entry.shortcutsCount);
+        }
+        mSubtitle.setText(subtitle);
+        mSubtitle.setVisibility(VISIBLE);
+    }
+
+    @Override
+    public void reapplyItemInfo(ItemInfoWithIcon info) {
+        if (getTag() == info) {
+            mIconLoadRequest = null;
+            mEnableIconUpdateAnimation = true;
+
+            // Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
+            info.bitmap.icon.prepareToDraw();
+
+            setIcon((PackageItemInfo) info);
+
+            mEnableIconUpdateAnimation = false;
+        }
+    }
+
+    /** Verifies that the current icon is high-res otherwise posts a request to load the icon. */
+    public void verifyHighRes() {
+        if (mIconLoadRequest != null) {
+            mIconLoadRequest.cancel();
+            mIconLoadRequest = null;
+        }
+        if (getTag() instanceof ItemInfoWithIcon) {
+            ItemInfoWithIcon info = (ItemInfoWithIcon) getTag();
+            if (info.usingLowResIcon()) {
+                mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache()
+                        .updateIconInBackground(this, info);
+            }
+        }
+    }
+
+    /** A listener for the widget section expansion / collapse events. */
+    public interface OnExpansionChangeListener {
+        /** Notifies that the widget section is expanded or collapsed. */
+        void onExpansionChange(boolean isExpanded);
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeaderHolder.java b/src/com/android/launcher3/widget/picker/WidgetsListHeaderHolder.java
new file mode 100644
index 0000000..d4e1b1c
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeaderHolder.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+/**
+ * A {@link ViewHolder} for {@link WidgetsListHeader} of an app, which renders the app icon, the app
+ * name, label and a button for showing / hiding widgets.
+ */
+public final class WidgetsListHeaderHolder extends ViewHolder {
+    final WidgetsListHeader mWidgetsListHeader;
+
+    public WidgetsListHeaderHolder(WidgetsListHeader view) {
+        super(view);
+
+        mWidgetsListHeader = view;
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java
new file mode 100644
index 0000000..ed53e6f
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import com.android.launcher3.R;
+import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+
+/**
+ * Binds data from {@link WidgetsListHeaderEntry} to UI elements in {@link WidgetsListHeaderHolder}.
+ */
+public final class WidgetsListHeaderViewHolderBinder implements
+        ViewHolderBinder<WidgetsListHeaderEntry, WidgetsListHeaderHolder> {
+    private final LayoutInflater mLayoutInflater;
+    private final OnHeaderClickListener mOnHeaderClickListener;
+
+    public WidgetsListHeaderViewHolderBinder(LayoutInflater layoutInflater,
+            OnHeaderClickListener onHeaderClickListener) {
+        mLayoutInflater = layoutInflater;
+        mOnHeaderClickListener = onHeaderClickListener;
+    }
+
+    @Override
+    public WidgetsListHeaderHolder newViewHolder(ViewGroup parent) {
+        WidgetsListHeader header = (WidgetsListHeader) mLayoutInflater.inflate(
+                R.layout.widgets_list_row_header, parent, false);
+
+        return new WidgetsListHeaderHolder(header);
+    }
+
+    @Override
+    public void bindViewHolder(WidgetsListHeaderHolder viewHolder, WidgetsListHeaderEntry data) {
+        WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
+        widgetsListHeader.applyFromItemInfoWithIcon(data);
+        widgetsListHeader.setExpanded(data.isWidgetListShown());
+        widgetsListHeader.setOnExpandChangeListener(isExpanded ->
+                mOnHeaderClickListener.onHeaderClicked(isExpanded, data.mPkgItem.packageName));
+    }
+
+    /** A listener to be invoked when {@link WidgetsListHeader} is clicked. */
+    public interface OnHeaderClickListener {
+        /** Calls when {@link WidgetsListHeader} is clicked to show / hide widgets for a package. */
+        void onHeaderClicked(boolean showWidgets, String packageName);
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
new file mode 100644
index 0000000..47fa71a
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.view.ViewGroup;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+
+import com.android.launcher3.R;
+import com.android.launcher3.WidgetPreviewLoader;
+import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.widget.WidgetCell;
+import com.android.launcher3.widget.WidgetImageView;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.util.WidgetsTableUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Binds data from {@link WidgetsListContentEntry} to UI elements in {@link WidgetsRowViewHolder}.
+ */
+public final class WidgetsListTableViewHolderBinder
+        implements ViewHolderBinder<WidgetsListContentEntry, WidgetsRowViewHolder> {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "WidgetsListRowViewHolderBinder";
+
+    private int mMaxSpansPerRow = 4;
+    private final LayoutInflater mLayoutInflater;
+    private final int mIndent;
+    private final OnClickListener mIconClickListener;
+    private final OnLongClickListener mIconLongClickListener;
+    private final WidgetPreviewLoader mWidgetPreviewLoader;
+    private boolean mApplyBitmapDeferred = false;
+
+    public WidgetsListTableViewHolderBinder(
+            Context context,
+            LayoutInflater layoutInflater,
+            OnClickListener iconClickListener,
+            OnLongClickListener iconLongClickListener,
+            WidgetPreviewLoader widgetPreviewLoader) {
+        mLayoutInflater = layoutInflater;
+        mIndent = context.getResources().getDimensionPixelSize(R.dimen.widget_section_indent);
+        mIconClickListener = iconClickListener;
+        mIconLongClickListener = iconLongClickListener;
+        mWidgetPreviewLoader = widgetPreviewLoader;
+    }
+
+    /**
+     * Defers applying bitmap on all the {@link WidgetCell} at
+     * {@link #bindViewHolder(WidgetsRowViewHolder, WidgetsListContentEntry)} if
+     * {@code applyBitmapDeferred} is {@code true}.
+     */
+    public void setApplyBitmapDeferred(boolean applyBitmapDeferred) {
+        mApplyBitmapDeferred = applyBitmapDeferred;
+    }
+
+    public void setMaxSpansPerRow(int maxSpansPerRow) {
+        mMaxSpansPerRow = maxSpansPerRow;
+    }
+
+    @Override
+    public WidgetsRowViewHolder newViewHolder(ViewGroup parent) {
+        if (DEBUG) {
+            Log.v(TAG, "\nonCreateViewHolder");
+        }
+
+        ViewGroup container = (ViewGroup) mLayoutInflater.inflate(
+                R.layout.widgets_table_container, parent, false);
+
+        // if the end padding is 0, then container view (horizontal scroll view) doesn't respect
+        // the end of the linear layout width + the start padding and doesn't allow scrolling.
+        container.findViewById(R.id.widgets_table).setPaddingRelative(mIndent, 0, 1, 0);
+
+        return new WidgetsRowViewHolder(container);
+    }
+
+    @Override
+    public void bindViewHolder(WidgetsRowViewHolder holder, WidgetsListContentEntry entry) {
+        TableLayout table = holder.mTableContainer;
+        if (DEBUG) {
+            Log.d(TAG, String.format("onBindViewHolder [widget#=%d, table.getChildCount=%d]",
+                    entry.mWidgets.size(), table.getChildCount()));
+        }
+
+        List<ArrayList<WidgetItem>> widgetItemsTable =
+                WidgetsTableUtils.groupWidgetItemsIntoTable(entry.mWidgets, mMaxSpansPerRow);
+        recycleTableBeforeBinding(table, widgetItemsTable);
+        // Bind the widget items.
+        for (int i = 0; i < widgetItemsTable.size(); i++) {
+            List<WidgetItem> widgetItemsPerRow = widgetItemsTable.get(i);
+            for (int j = 0; j < widgetItemsPerRow.size(); j++) {
+                TableRow row = (TableRow) table.getChildAt(i);
+                row.setVisibility(View.VISIBLE);
+                WidgetCell widget = (WidgetCell) row.getChildAt(j);
+                widget.clear();
+                WidgetItem widgetItem = widgetItemsPerRow.get(j);
+                widget.setPreviewSize(widgetItem.spanX, widgetItem.spanY);
+                widget.applyFromCellItem(widgetItem, mWidgetPreviewLoader);
+                widget.setApplyBitmapDeferred(mApplyBitmapDeferred);
+                widget.ensurePreview();
+                widget.setVisibility(View.VISIBLE);
+            }
+        }
+    }
+
+    /**
+     * Adds and hides table rows and columns from {@code table} to ensure there is sufficient room
+     * to display {@code widgetItemsTable}.
+     *
+     * <p>Instead of recreating all UI elements in {@code table}, this function recycles all
+     * existing UI elements. Instead of deleting excessive elements, it hides them.
+     */
+    private void recycleTableBeforeBinding(TableLayout table,
+            List<ArrayList<WidgetItem>> widgetItemsTable) {
+        // Hide extra table rows.
+        for (int i = widgetItemsTable.size(); i < table.getChildCount(); i++) {
+            table.getChildAt(i).setVisibility(View.GONE);
+        }
+
+        for (int i = 0; i < widgetItemsTable.size(); i++) {
+            List<WidgetItem> widgetItems = widgetItemsTable.get(i);
+            TableRow tableRow;
+            if (i < table.getChildCount()) {
+                tableRow = (TableRow) table.getChildAt(i);
+            } else {
+                tableRow = new TableRow(table.getContext());
+                tableRow.setGravity(Gravity.CENTER_VERTICAL);
+                table.addView(tableRow);
+            }
+            if (tableRow.getChildCount() > widgetItems.size()) {
+                for (int j = widgetItems.size(); j < tableRow.getChildCount(); j++) {
+                    tableRow.getChildAt(j).setVisibility(View.GONE);
+                }
+            } else {
+                for (int j = tableRow.getChildCount(); j < widgetItems.size(); j++) {
+                    WidgetCell widget = (WidgetCell) mLayoutInflater.inflate(
+                            R.layout.widget_cell, tableRow, false);
+                    // set up touch.
+                    WidgetImageView preview = widget.findViewById(R.id.widget_preview);
+                    preview.setOnClickListener(mIconClickListener);
+                    preview.setOnLongClickListener(mIconLongClickListener);
+                    tableRow.addView(widget);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void unbindViewHolder(WidgetsRowViewHolder holder) {
+        int numOfRows = holder.mTableContainer.getChildCount();
+        for (int i = 0; i < numOfRows; i++) {
+            TableRow tableRow = (TableRow) holder.mTableContainer.getChildAt(i);
+            int numOfCols = tableRow.getChildCount();
+            for (int j = 0; j < numOfCols; j++) {
+                WidgetCell widget = (WidgetCell) tableRow.getChildAt(j);
+                widget.clear();
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
similarity index 83%
rename from src/com/android/launcher3/widget/WidgetsRecyclerView.java
rename to src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
index 69de12b..d65a809 100644
--- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.widget;
+package com.android.launcher3.widget.picker;
 
 import android.content.Context;
 import android.graphics.Point;
@@ -22,13 +22,13 @@
 import android.view.MotionEvent;
 import android.view.View;
 
-import com.android.launcher3.BaseRecyclerView;
-import com.android.launcher3.R;
-
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
 
+import com.android.launcher3.BaseRecyclerView;
+import com.android.launcher3.R;
+
 /**
  * The widgets recycler view.
  */
@@ -40,6 +40,7 @@
 
     private final Point mFastScrollerOffset = new Point();
     private boolean mTouchDownOnScroller;
+    private HeaderViewDimensionsProvider mHeaderViewDimensionsProvider;
 
     public WidgetsRecyclerView(Context context) {
         this(context, null);
@@ -89,7 +90,7 @@
         LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager());
         layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
 
-        int posInt = (int) ((touchFraction == 1)? pos -1 : pos);
+        int posInt = (int) ((touchFraction == 1) ? pos - 1 : pos);
         return mAdapter.getSectionName(posInt);
     }
 
@@ -135,8 +136,8 @@
     @Override
     protected int getAvailableScrollHeight() {
         View child = getChildAt(0);
-        return child.getMeasuredHeight() * mAdapter.getItemCount() - getScrollbarTrackHeight()
-                - mScrollbarTop;
+        return child.getMeasuredHeight() * mAdapter.getItemCount() + getScrollBarTop()
+                + getPaddingBottom() - mScrollbar.getHeight();
     }
 
     private boolean isModelNotReady() {
@@ -145,7 +146,9 @@
 
     @Override
     public int getScrollBarTop() {
-        return mScrollbarTop;
+        return mHeaderViewDimensionsProvider == null
+                ? mScrollbarTop
+                : mHeaderViewDimensionsProvider.getHeaderViewHeight() + mScrollbarTop;
     }
 
     @Override
@@ -171,4 +174,21 @@
     @Override
     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
     }
-}
\ No newline at end of file
+
+    public void setHeaderViewDimensionsProvider(
+            HeaderViewDimensionsProvider headerViewDimensionsProvider) {
+        mHeaderViewDimensionsProvider = headerViewDimensionsProvider;
+    }
+
+    /**
+     * Provides dimensions of the header view that is shown at the top of a
+     * {@link WidgetsRecyclerView}.
+     */
+    public interface HeaderViewDimensionsProvider {
+        /**
+         * Returns the height, in pixels, of the header view that is shown at the top of a
+         * {@link WidgetsRecyclerView}.
+         */
+        int getHeaderViewHeight();
+    }
+}
diff --git a/src/com/android/launcher3/widget/WidgetsRowViewHolder.java b/src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java
similarity index 68%
rename from src/com/android/launcher3/widget/WidgetsRowViewHolder.java
rename to src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java
index d26edb6..aef1103 100644
--- a/src/com/android/launcher3/widget/WidgetsRowViewHolder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java
@@ -13,25 +13,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.launcher3.widget;
+package com.android.launcher3.widget.picker;
 
 import android.view.ViewGroup;
-
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.R;
+import android.widget.TableLayout;
 
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 
-public class WidgetsRowViewHolder extends ViewHolder {
+import com.android.launcher3.R;
 
-    public final ViewGroup cellContainer;
-    public final BubbleTextView title;
+/** A {@link ViewHolder} for showing widgets of an app in the full widget picker. */
+public final class WidgetsRowViewHolder extends ViewHolder {
+
+    public final TableLayout mTableContainer;
 
     public WidgetsRowViewHolder(ViewGroup v) {
         super(v);
 
-        cellContainer = v.findViewById(R.id.widgets_cell_list);
-        title = v.findViewById(R.id.section);
-        title.setAccessibilityDelegate(null);
+        mTableContainer = v.findViewById(R.id.widgets_table);
     }
 }
diff --git a/src/com/android/launcher3/widget/picker/search/WidgetsPickerSearchPipeline.java b/src/com/android/launcher3/widget/picker/search/WidgetsPickerSearchPipeline.java
new file mode 100644
index 0000000..d12782c
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/WidgetsPickerSearchPipeline.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 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.widget.picker.search;
+
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * An interface for a pipeline to handle widgets search.
+ */
+public interface WidgetsPickerSearchPipeline {
+
+    /**
+     * Performs a search query asynchronically. Invokes {@code callback} when the search is
+     * complete.
+     */
+    void query(String input, Consumer<List<WidgetsListBaseEntry>> callback);
+
+    /**
+     * Cancels any ongoing search request.
+     */
+    default void cancel() {};
+
+    /**
+     * Cleans up after search is no longer needed.
+     */
+    default void destroy() {};
+}
diff --git a/src/com/android/launcher3/widget/util/WidgetsTableUtils.java b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
new file mode 100644
index 0000000..e73d661
--- /dev/null
+++ b/src/com/android/launcher3/widget/util/WidgetsTableUtils.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 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.widget.util;
+
+import com.android.launcher3.model.WidgetItem;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** An utility class which groups {@link WidgetItem}s into a table. */
+public final class WidgetsTableUtils {
+
+    /**
+     * Groups widgets in the following order:
+     * 1. Widgets always go before shortcuts.
+     * 2. Widgets with smaller horizontal spans will be shown first.
+     * 3. If widgets have the same horizontal spans, then widgets with a smaller vertical spans will
+     *    go first.
+     * 4. If both widgets have the same horizontal and vertical spans, they will use the same order
+     *    from the given {@code widgetItems}.
+     */
+    private static final Comparator<WidgetItem> WIDGET_SHORTCUT_COMPARATOR = (item, otherItem) -> {
+        if (item.widgetInfo != null && otherItem.widgetInfo == null) return -1;
+
+        if (item.widgetInfo == null && otherItem.widgetInfo != null) return 1;
+        if (item.spanX == otherItem.spanX) {
+            if (item.spanY == otherItem.spanY) return 0;
+            return item.spanY > otherItem.spanY ? 1 : -1;
+        }
+        return item.spanX > otherItem.spanX ? 1 : -1;
+    };
+
+
+    /**
+     * Groups widgets items into a 2D array which matches their appearance in a UI table.
+     *
+     * <p>Grouping:
+     * 1. Widgets and shortcuts never group together in the same row.
+     * 2. The ordered widgets are grouped together in the same row until their total horizontal
+     *    spans exceed the {@code maxSpansPerRow}.
+     * 3. The order shortcuts are grouped together in the same row until their total horizontal
+     *    spans exceed the {@code maxSpansPerRow}.
+     */
+    public static List<ArrayList<WidgetItem>> groupWidgetItemsIntoTable(
+            List<WidgetItem> widgetItems, final int maxSpansPerRow) {
+        List<WidgetItem> sortedWidgetItems = widgetItems.stream().sorted(WIDGET_SHORTCUT_COMPARATOR)
+                .collect(Collectors.toList());
+        List<ArrayList<WidgetItem>> widgetItemsTable = new ArrayList<>();
+        ArrayList<WidgetItem> widgetItemsAtRow = null;
+        for (WidgetItem widgetItem : sortedWidgetItems) {
+            if (widgetItemsAtRow == null) {
+                widgetItemsAtRow = new ArrayList<>();
+                widgetItemsTable.add(widgetItemsAtRow);
+            }
+            int numOfWidgetItems = widgetItemsAtRow.size();
+            int totalHorizontalSpan = widgetItemsAtRow.stream().map(item -> item.spanX)
+                    .reduce(/* default= */ 0, Integer::sum);
+            if (numOfWidgetItems == 0) {
+                widgetItemsAtRow.add(widgetItem);
+            } else if (widgetItem.spanX + totalHorizontalSpan <= maxSpansPerRow
+                    && widgetItem.hasSameType(widgetItemsAtRow.get(numOfWidgetItems - 1))) {
+                // Group items in the same row if
+                // 1. they are with the same type, i.e. a row can only have widgets or shortcuts but
+                //    never a mix of both.
+                // 2. the total number of horizontal spans are smaller than or equal to
+                //    MAX_SPAN_PER_ROW. If an item has a horizontal span > MAX_SPAN_PER_ROW, we just
+                //    place it in its own row regardless of the horizontal span limit.
+                widgetItemsAtRow.add(widgetItem);
+            } else {
+                widgetItemsAtRow = new ArrayList<>();
+                widgetItemsTable.add(widgetItemsAtRow);
+                widgetItemsAtRow.add(widgetItem);
+            }
+        }
+        return widgetItemsTable;
+    }
+}
diff --git a/src/com/android/launcher3/workprofile/PersonalWorkPagedView.java b/src/com/android/launcher3/workprofile/PersonalWorkPagedView.java
new file mode 100644
index 0000000..8b05a0d
--- /dev/null
+++ b/src/com/android/launcher3/workprofile/PersonalWorkPagedView.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 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.workprofile;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.android.launcher3.PagedView;
+
+/**
+ *  A {@link PagedView} for showing different views for the personal and work profile respectively.
+ */
+public class PersonalWorkPagedView extends PagedView<PersonalWorkSlidingTabStrip> {
+
+    static final float START_DAMPING_TOUCH_SLOP_ANGLE = (float) Math.PI / 6;
+    static final float MAX_SWIPE_ANGLE = (float) Math.PI / 3;
+    static final float TOUCH_SLOP_DAMPING_FACTOR = 4;
+
+    public PersonalWorkPagedView(Context context) {
+        this(context, null);
+    }
+
+    public PersonalWorkPagedView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public PersonalWorkPagedView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected String getCurrentPageDescription() {
+        // Not necessary, tab-bar already has two tabs with their own descriptions.
+        return "";
+    }
+
+    @Override
+    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+        super.onScrollChanged(l, t, oldl, oldt);
+        mPageIndicator.setScroll(l, mMaxScroll);
+    }
+
+    @Override
+    protected void determineScrollingStart(MotionEvent ev) {
+        float absDeltaX = Math.abs(ev.getX() - getDownMotionX());
+        float absDeltaY = Math.abs(ev.getY() - getDownMotionY());
+
+        if (Float.compare(absDeltaX, 0f) == 0) return;
+
+        float slope = absDeltaY / absDeltaX;
+        float theta = (float) Math.atan(slope);
+
+        if (absDeltaX > mTouchSlop || absDeltaY > mTouchSlop) {
+            cancelCurrentPageLongPress();
+        }
+
+        if (theta > MAX_SWIPE_ANGLE) {
+            return;
+        } else if (theta > START_DAMPING_TOUCH_SLOP_ANGLE) {
+            theta -= START_DAMPING_TOUCH_SLOP_ANGLE;
+            float extraRatio = (float)
+                    Math.sqrt((theta / (MAX_SWIPE_ANGLE - START_DAMPING_TOUCH_SLOP_ANGLE)));
+            super.determineScrollingStart(ev, 1 + TOUCH_SLOP_DAMPING_FACTOR * extraRatio);
+        } else {
+            super.determineScrollingStart(ev);
+        }
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    @Override
+    protected boolean canScroll(float absVScroll, float absHScroll) {
+        return (absHScroll > absVScroll) && super.canScroll(absVScroll, absHScroll);
+    }
+}
diff --git a/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
similarity index 85%
rename from src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java
rename to src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
index 2de425e..3a3028f 100644
--- a/src/com/android/launcher3/allapps/PersonalWorkSlidingTabStrip.java
+++ b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.launcher3.allapps;
+package com.android.launcher3.workprofile;
 
 import android.content.Context;
 import android.graphics.Canvas;
@@ -35,9 +35,6 @@
  * Supports two indicator colors, dedicated for personal and work tabs.
  */
 public class PersonalWorkSlidingTabStrip extends LinearLayout implements PageIndicator {
-    private static final int POSITION_PERSONAL = 0;
-    private static final int POSITION_WORK = 1;
-
     private final Paint mSelectedIndicatorPaint;
     private final Paint mDividerPaint;
 
@@ -47,7 +44,7 @@
     private float mScrollOffset;
     private int mSelectedPosition = 0;
 
-    private AllAppsContainerView mContainerView;
+    private OnActivePageChangedListener mOnActivePageChangedListener;
     private int mLastActivePage = 0;
     private boolean mIsRtl;
 
@@ -123,7 +120,7 @@
         float y = getHeight() - mDividerPaint.getStrokeWidth();
         canvas.drawLine(getPaddingLeft(), y, getWidth() - getPaddingRight(), y, mDividerPaint);
         canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
-            mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
+                mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
     }
 
     @Override
@@ -135,15 +132,15 @@
     @Override
     public void setActiveMarker(int activePage) {
         updateTabTextColor(activePage);
-        if (mContainerView != null && mLastActivePage != activePage) {
+        if (mOnActivePageChangedListener != null && mLastActivePage != activePage) {
             updateIndicatorPosition(activePage);
-            mContainerView.onTabChanged(activePage);
+            mOnActivePageChangedListener.onActivePageChanged(activePage);
         }
         mLastActivePage = activePage;
     }
 
-    public void setContainerView(AllAppsContainerView containerView) {
-        mContainerView = containerView;
+    public void setOnActivePageChangedListener(OnActivePageChangedListener listener) {
+        mOnActivePageChangedListener = listener;
     }
 
     @Override
@@ -153,4 +150,12 @@
     public boolean hasOverlappingRendering() {
         return false;
     }
+
+    /**
+     * Interface definition for a callback to be invoked when an active page has been changed.
+     */
+    public interface OnActivePageChangedListener {
+        /** Called when the active page has been changed. */
+        void onActivePageChanged(int currentActivePage);
+    }
 }
diff --git a/src_build_config/BuildConfig.java b/src_build_config/com/android/launcher3/BuildConfig.java
similarity index 100%
rename from src_build_config/BuildConfig.java
rename to src_build_config/com/android/launcher3/BuildConfig.java
diff --git a/src_plugins/com/android/systemui/plugins/AllAppsSearchPlugin.java b/src_plugins/com/android/systemui/plugins/AllAppsSearchPlugin.java
deleted file mode 100644
index 0b48c07..0000000
--- a/src_plugins/com/android/systemui/plugins/AllAppsSearchPlugin.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.plugins;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.os.CancellationSignal;
-import android.os.Parcelable;
-import android.view.View;
-
-import com.android.systemui.plugins.annotations.ProvidesInterface;
-import com.android.systemui.plugins.shared.SearchTargetEventLegacy;
-import com.android.systemui.plugins.shared.SearchTargetLegacy;
-
-import java.util.List;
-import java.util.function.Consumer;
-
-/**
- * Implement this plugin interface to fetch search result data from the plugin side.
- */
-@ProvidesInterface(action = AllAppsSearchPlugin.ACTION, version = AllAppsSearchPlugin.VERSION)
-public interface AllAppsSearchPlugin extends Plugin {
-    String ACTION = "com.android.systemui.action.PLUGIN_ALL_APPS_SEARCH_ACTIONS";
-    int VERSION = 9;
-
-    /**
-     * init plugin
-     */
-    void setup(Activity activity, View view, boolean useLegacy);
-
-    /**
-     * Send launcher state related signals.
-     */
-    void onStateTransitionStart(int fromState, int toState);
-
-    void onStateTransitionComplete(int state);
-
-    /**
-     * Send launcher window focus and visibility changed signals.
-     */
-    void onWindowFocusChanged(boolean hasFocus);
-
-    void onWindowVisibilityChanged(int visibility);
-
-    /**
-     * Send signal when user starts typing, perform search, notify search target
-     * event when search ends.
-     */
-    void startedSearchSession();
-
-    /**
-     * Main function that triggers search.
-     *
-     * @param input              string that has been typed by a user
-     * @param inputArgs          extra info that may be relevant for the input query
-     * @param results            contains the result that will be rendered in all apps search
-     *                           surface
-     * @param cancellationSignal {@link CancellationSignal} can be used to share status of current
-     */
-    void queryLegacy(String input, Bundle inputArgs, Consumer<List<SearchTargetLegacy>> results,
-            CancellationSignal cancellationSignal);
-
-    /**
-     * Main function that triggers search.
-     *
-     * @param input              string that has been typed by a user
-     * @param inputArgs          extra info that may be relevant for the input query
-     * @param results            contains the result that will be rendered in all apps search
-     *                           surface
-     * @param cancellationSignal {@link CancellationSignal} can be used to share status of current
-     */
-    void query(String input, Bundle inputArgs, Consumer<List<Parcelable>> results,
-            CancellationSignal cancellationSignal);
-
-    /**
-     * Send over search target interaction events to Plugin
-     */
-    void notifySearchTargetEventLegacy(SearchTargetEventLegacy event);
-
-    /**
-     * Send over search target interaction events to Plugin
-     */
-    void notifySearchTargetEvent(Parcelable event);
-
-    /**
-     * Launcher activity lifecycle callbacks
-     */
-    void onResume(int state);
-
-    void onStop(int state);
-}
\ No newline at end of file
diff --git a/src_plugins/com/android/systemui/plugins/BcSmartspaceDataPlugin.java b/src_plugins/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
new file mode 100644
index 0000000..f8a9a04
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 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.systemui.plugins;
+
+import android.os.Parcelable;
+
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
+import java.util.List;
+
+/**
+ * Interface to provide SmartspaceTargets to BcSmartspace.
+ */
+@ProvidesInterface(action = BcSmartspaceDataPlugin.ACTION, version = BcSmartspaceDataPlugin.VERSION)
+public interface BcSmartspaceDataPlugin extends Plugin {
+    String ACTION = "com.android.systemui.action.PLUGIN_BC_SMARTSPACE_DATA";
+    int VERSION = 1;
+
+    /** Register a listener to get Smartspace data. */
+    void registerListener(SmartspaceTargetListener listener);
+
+    /** Unregister a listener. */
+    void unregisterListener(SmartspaceTargetListener listener);
+
+    /** Provides Smartspace data to registered listeners. */
+    interface SmartspaceTargetListener {
+        /** Each Parcelable is a SmartspaceTarget that represents a card. */
+        void onSmartspaceTargetsUpdated(List<? extends Parcelable> targets);
+    }
+}
diff --git a/src_plugins/com/android/systemui/plugins/shared/SearchTargetEventLegacy.java b/src_plugins/com/android/systemui/plugins/shared/SearchTargetEventLegacy.java
deleted file mode 100644
index 7fbd6ac..0000000
--- a/src_plugins/com/android/systemui/plugins/shared/SearchTargetEventLegacy.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.plugins.shared;
-
-import android.os.Bundle;
-
-/**
- * Event used for the feedback loop to the plugin. (and future aiai)
- *
- * @deprecated Use SearchTargetEvent
- */
-@Deprecated
-public class SearchTargetEventLegacy {
-    public static final int POSITION_NONE = -1;
-
-    public static final int SELECT = 0;
-    public static final int QUICK_SELECT = 1;
-    public static final int LONG_PRESS = 2;
-    public static final int CHILD_SELECT = 3;
-
-    private final SearchTargetLegacy mSearchTarget;
-    private final int mEventType;
-    private final int mShortcutPosition;
-    private final Bundle mExtras;
-
-    public SearchTargetEventLegacy(SearchTargetLegacy searchTarget, int eventType,
-            int shortcutPosition,
-            Bundle extras) {
-        mSearchTarget = searchTarget;
-        mEventType = eventType;
-        mShortcutPosition = shortcutPosition;
-        mExtras = extras;
-    }
-
-
-    public SearchTargetLegacy getSearchTarget() {
-        return mSearchTarget;
-    }
-
-    public int getShortcutPosition() {
-        return mShortcutPosition;
-    }
-
-    public int getEventType() {
-        return mEventType;
-    }
-
-    public Bundle getExtras() {
-        return mExtras;
-    }
-
-    /**
-     * A builder for {@link SearchTargetLegacy}
-     */
-    public static final class Builder {
-        private final SearchTargetLegacy mSearchTarget;
-        private final int mEventType;
-        private int mShortcutPosition = POSITION_NONE;
-        private Bundle mExtras;
-
-        public Builder(SearchTargetLegacy searchTarget, int eventType) {
-            mSearchTarget = searchTarget;
-            mEventType = eventType;
-        }
-
-        public Builder setShortcutPosition(int shortcutPosition) {
-            mShortcutPosition = shortcutPosition;
-            return this;
-        }
-
-        public Builder setExtras(Bundle extras) {
-            mExtras = extras;
-            return this;
-        }
-
-        public SearchTargetEventLegacy build() {
-            return new SearchTargetEventLegacy(mSearchTarget, mEventType, mShortcutPosition,
-                    mExtras);
-        }
-    }
-
-}
diff --git a/src_plugins/com/android/systemui/plugins/shared/SearchTargetLegacy.java b/src_plugins/com/android/systemui/plugins/shared/SearchTargetLegacy.java
deleted file mode 100644
index 2a6ba88..0000000
--- a/src_plugins/com/android/systemui/plugins/shared/SearchTargetLegacy.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.plugins.shared;
-
-import android.app.RemoteAction;
-import android.content.ComponentName;
-import android.content.pm.ShortcutInfo;
-import android.os.Bundle;
-import android.os.UserHandle;
-
-import java.util.List;
-
-/**
- * Used to return all apps search targets.
- *
- * @deprecated Use SearchTarget
- */
-@Deprecated
-public class SearchTargetLegacy implements Comparable<SearchTargetLegacy> {
-
-    private final String mItemId;
-    private final String mItemType;
-    private final float mScore;
-
-    private final ComponentName mComponentName;
-    private final UserHandle mUserHandle;
-    private final List<ShortcutInfo> mShortcutInfos;
-    //TODO: (sfufa) replace with a list of a custom type
-    private final RemoteAction mRemoteAction;
-    private final Bundle mExtras;
-
-    private SearchTargetLegacy(String itemId, String itemType, float score,
-            ComponentName componentName, UserHandle userHandle, List<ShortcutInfo> shortcutInfos,
-            RemoteAction remoteAction, Bundle extras) {
-        mItemId = itemId;
-        mItemType = itemType;
-        mScore = score;
-        mComponentName = componentName;
-        mUserHandle = userHandle;
-        mShortcutInfos = shortcutInfos;
-        mExtras = extras;
-        mRemoteAction = remoteAction;
-    }
-
-    public String getItemId() {
-        return mItemId;
-    }
-
-    public String getItemType() {
-        return mItemType;
-    }
-
-    public ComponentName getComponentName() {
-        return mComponentName;
-    }
-
-    public UserHandle getUserHandle() {
-        return mUserHandle;
-    }
-
-    public float getScore() {
-        return mScore;
-    }
-
-    public List<ShortcutInfo> getShortcutInfos() {
-        return mShortcutInfos;
-    }
-
-    public Bundle getExtras() {
-        return mExtras;
-    }
-
-    public RemoteAction getRemoteAction() {
-        return mRemoteAction;
-    }
-
-    @Override
-    public int compareTo(SearchTargetLegacy o) {
-        return Float.compare(o.mScore, mScore);
-    }
-
-    /**
-     * A builder for {@link SearchTargetLegacy}
-     */
-    public static final class Builder {
-
-
-        private String mItemId;
-
-        private final String mItemType;
-        private final float mScore;
-
-
-        private ComponentName mComponentName;
-        private UserHandle mUserHandle;
-        private List<ShortcutInfo> mShortcutInfos;
-        private Bundle mExtras;
-        private RemoteAction mRemoteAction;
-
-        public Builder(String itemType, float score) {
-            this(itemType, score, null, null);
-        }
-
-        public Builder(String itemType, float score, ComponentName cn,
-                UserHandle user) {
-            mItemType = itemType;
-            mScore = score;
-            mComponentName = cn;
-            mUserHandle = user;
-        }
-
-        public String getItemId() {
-            return mItemId;
-        }
-
-        public float getScore() {
-            return mScore;
-        }
-
-        public Builder setItemId(String itemId) {
-            mItemId = itemId;
-            return this;
-        }
-
-        public Builder setComponentName(ComponentName componentName) {
-            mComponentName = componentName;
-            return this;
-        }
-
-        public Builder setUserHandle(UserHandle userHandle) {
-            mUserHandle = userHandle;
-            return this;
-        }
-
-        public Builder setShortcutInfos(List<ShortcutInfo> shortcutInfos) {
-            mShortcutInfos = shortcutInfos;
-            return this;
-        }
-
-        public Builder setExtras(Bundle extras) {
-            mExtras = extras;
-            return this;
-        }
-
-        public Builder setRemoteAction(RemoteAction remoteAction) {
-            mRemoteAction = remoteAction;
-            return this;
-        }
-
-        /**
-         * Builds a {@link SearchTargetLegacy}
-         */
-        public SearchTargetLegacy build() {
-            if (mItemId == null) {
-                throw new IllegalStateException("Item ID is required for building SearchTarget");
-            }
-            return new SearchTargetLegacy(mItemId, mItemType, mScore, mComponentName, mUserHandle,
-                    mShortcutInfos,
-                    mRemoteAction, mExtras);
-        }
-    }
-}
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
index 269af7b..abce2a2 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java
@@ -21,10 +21,10 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
-import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 
 /**
  * Helper class to handle results of {@link com.android.launcher3.model.LoaderTask}.
@@ -47,8 +47,8 @@
 
     @Override
     public void bindWidgets() {
-        final ArrayList<WidgetListRowEntry> widgets =
-                mBgDataModel.widgetsModel.getWidgetsList(mApp.getContext());
+        final List<WidgetsListBaseEntry> widgets =
+                mBgDataModel.widgetsModel.getWidgetsListForPicker(mApp.getContext());
         executeCallbacksTask(c -> c.bindAllWidgets(widgets), mUiExecutor);
     }
 }
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
index b4e45f8..3ea4766 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
@@ -5,11 +5,12 @@
 
 import static com.android.launcher3.pm.ShortcutConfigActivityInfo.queryList;
 
+import static java.util.stream.Collectors.toList;
+
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
-import android.os.Process;
 import android.os.UserHandle;
 import android.util.Log;
 
@@ -18,7 +19,6 @@
 import com.android.launcher3.AppFilter;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.AlphabeticIndexCompat;
 import com.android.launcher3.config.FeatureFlags;
@@ -28,12 +28,14 @@
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
-import com.android.launcher3.widget.WidgetItemComparator;
-import com.android.launcher3.widget.WidgetListRowEntry;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.model.WidgetsListContentEntry;
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
+import com.android.launcher3.widget.picker.WidgetsDiffReporter;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -60,29 +62,44 @@
     private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsList = new HashMap<>();
 
     /**
-     * Returns a list of {@link WidgetListRowEntry}. All {@link WidgetItem} in a single row
-     * are sorted (based on label and user), but the overall list of {@link WidgetListRowEntry}s
-     * is not sorted. This list is sorted at the UI when using
-     * {@link com.android.launcher3.widget.WidgetsDiffReporter}
+     * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row
+     * are sorted (based on label and user), but the overall list of
+     * {@link WidgetsListBaseEntry}s is not sorted. This list is sorted at the UI when using
+     * {@link WidgetsDiffReporter}
      *
-     * @see com.android.launcher3.widget.WidgetsListAdapter#setWidgets(ArrayList)
+     * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
      */
-    public synchronized ArrayList<WidgetListRowEntry> getWidgetsList(Context context) {
-        ArrayList<WidgetListRowEntry> result = new ArrayList<>();
+    public synchronized ArrayList<WidgetsListBaseEntry> getWidgetsListForPicker(Context context) {
+        ArrayList<WidgetsListBaseEntry> result = new ArrayList<>();
         AlphabeticIndexCompat indexer = new AlphabeticIndexCompat(context);
 
-        WidgetItemComparator widgetComparator = new WidgetItemComparator();
         for (Map.Entry<PackageItemInfo, List<WidgetItem>> entry : mWidgetsList.entrySet()) {
-            WidgetListRowEntry row = new WidgetListRowEntry(
-                    entry.getKey(), new ArrayList<>(entry.getValue()));
-            row.titleSectionName = (row.pkgItem.title == null) ? "" :
-                    indexer.computeSectionName(row.pkgItem.title);
-            Collections.sort(row.widgets, widgetComparator);
-            result.add(row);
+            PackageItemInfo pkgItem = entry.getKey();
+            List<WidgetItem> widgetItems = entry.getValue();
+            String sectionName = (pkgItem.title == null) ? "" :
+                    indexer.computeSectionName(pkgItem.title);
+            result.add(new WidgetsListHeaderEntry(pkgItem, sectionName, widgetItems));
+            result.add(new WidgetsListContentEntry(pkgItem, sectionName, widgetItems));
         }
         return result;
     }
 
+    /** Returns a mapping of packages to their widgets without static shortcuts. */
+    public synchronized Map<PackageUserKey, List<WidgetItem>> getAllWidgetsWithoutShortcuts() {
+        Map<PackageUserKey, List<WidgetItem>> packagesToWidgets = new HashMap<>();
+        mWidgetsList.forEach((packageItemInfo, widgetsAndShortcuts) -> {
+            List<WidgetItem> widgets = widgetsAndShortcuts.stream()
+                        .filter(item -> item.widgetInfo != null)
+                        .collect(toList());
+            if (widgets.size() > 0) {
+                packagesToWidgets.put(
+                        new PackageUserKey(packageItemInfo.packageName, packageItemInfo.user),
+                        widgets);
+            }
+        });
+        return packagesToWidgets;
+    }
+
     /**
      * @param packageUser If null, all widgets and shortcuts are updated and returned, otherwise
      *                    only widgets and shortcuts associated with the package/user are.
@@ -115,7 +132,7 @@
                 widgetsAndShortcuts.add(new WidgetItem(info, app.getIconCache(), pm));
                 updatedItems.add(info);
             }
-            setWidgetsAndShortcuts(widgetsAndShortcuts, app, packageUser);
+            setWidgetsAndShortcuts(widgetsAndShortcuts, app);
         } catch (Exception e) {
             if (!FeatureFlags.IS_STUDIO_BUILD && Utilities.isBinderSizeError(e)) {
                 // the returned value may be incomplete and will not be refreshed until the next
@@ -132,52 +149,28 @@
     }
 
     private synchronized void setWidgetsAndShortcuts(ArrayList<WidgetItem> rawWidgetsShortcuts,
-            LauncherAppState app, @Nullable PackageUserKey packageUser) {
+            LauncherAppState app) {
         if (DEBUG) {
             Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + rawWidgetsShortcuts.size());
         }
 
         // Temporary list for {@link PackageItemInfos} to avoid having to go through
         // {@link mPackageItemInfos} to locate the key to be used for {@link #mWidgetsList}
-        HashMap<String, PackageItemInfo> tmpPackageItemInfos = new HashMap<>();
+        HashMap<PackageUserKey, PackageItemInfo> tmpPackageItemInfos = new HashMap<>();
 
         // clear the lists.
-        if (packageUser == null) {
-            mWidgetsList.clear();
-        } else {
-            PackageItemInfo packageItem = mWidgetsList.keySet()
-                    .stream()
-                    .filter(item -> item.packageName.equals(packageUser.mPackageName))
-                    .findFirst()
-                    .orElse(null);
-            if (packageItem != null) {
-                // We want to preserve the user that was on the packageItem previously,
-                // so add it to tmpPackageItemInfos here to avoid creating a new entry.
-                tmpPackageItemInfos.put(packageItem.packageName, packageItem);
-
-                // Add the widgets for other users in the rawList as it only contains widgets for
-                // packageUser
-                List<WidgetItem> otherUserItems = mWidgetsList.remove(packageItem);
-                otherUserItems.removeIf(w -> w.user.equals(packageUser.mUser));
-                rawWidgetsShortcuts.addAll(otherUserItems);
-            }
-        }
-
-        UserHandle myUser = Process.myUserHandle();
-
+        mWidgetsList.clear();
         // add and update.
         mWidgetsList.putAll(rawWidgetsShortcuts.stream()
                 .filter(new WidgetValidityCheck(app))
                 .collect(Collectors.groupingBy(item -> {
-                    String packageName = item.componentName.getPackageName();
-                    PackageItemInfo pInfo = tmpPackageItemInfos.get(packageName);
+                    PackageUserKey packageUserKey = new PackageUserKey(
+                            item.componentName.getPackageName(), item.user);
+                    PackageItemInfo pInfo = tmpPackageItemInfos.get(packageUserKey);
                     if (pInfo == null) {
-                        pInfo = new PackageItemInfo(packageName);
+                        pInfo = new PackageItemInfo(packageUserKey.mPackageName);
                         pInfo.user = item.user;
-                        tmpPackageItemInfos.put(packageName,  pInfo);
-                    } else if (!myUser.equals(pInfo.user)) {
-                        // Keep updating the user, until we get the primary user.
-                        pInfo.user = item.user;
+                        tmpPackageItemInfos.put(packageUserKey,  pInfo);
                     }
                     return pInfo;
                 })));
diff --git a/tests/Android.mk b/tests/Android.mk
index 3d9077d..2c7d30a 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -32,11 +32,13 @@
 
     LOCAL_SRC_FILES := $(call all-java-files-under, tapl) \
         ../src/com/android/launcher3/ResourceUtils.java \
-        ../src/com/android/launcher3/util/SecureSettingsObserver.java \
         ../src/com/android/launcher3/testing/TestProtocol.java
 endif
 
 LOCAL_MODULE := ub-launcher-aosp-tapl
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../NOTICE
 LOCAL_SDK_VERSION := system_current
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index f243f27..744dee0 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -98,7 +98,7 @@
         <activity
             android:name="com.android.launcher3.testcomponent.TestLauncherActivity"
             android:clearTaskOnLaunch="true"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout"
             android:enabled="false"
             android:label="Test launcher"
             android:launchMode="singleTask"
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index b3c1240..0edfbed 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -151,23 +151,30 @@
     }
 
     public static String dumpHprofData() {
-        if (sDumpWasGenerated) return "dump has already been generated by another test";
-        try {
-            final String fileName = getInstrumentation().getTargetContext().getFilesDir().getPath()
-                    + "/ActivityLeakHeapDump.hprof";
-            if (TestHelpers.isInLauncherProcess()) {
-                Debug.dumpHprofData(fileName);
-            } else {
-                final UiDevice device = UiDevice.getInstance(getInstrumentation());
-                device.executeShellCommand(
-                        "am dumpheap " + device.getLauncherPackageName() + " " + fileName);
+        String result;
+        if (sDumpWasGenerated) {
+            result = "dump has already been generated by another test";
+        } else {
+            try {
+                final String fileName =
+                        getInstrumentation().getTargetContext().getFilesDir().getPath()
+                                + "/ActivityLeakHeapDump.hprof";
+                if (TestHelpers.isInLauncherProcess()) {
+                    Debug.dumpHprofData(fileName);
+                } else {
+                    final UiDevice device = UiDevice.getInstance(getInstrumentation());
+                    device.executeShellCommand(
+                            "am dumpheap " + device.getLauncherPackageName() + " " + fileName);
+                }
+                sDumpWasGenerated = true;
+                result = "memory dump filename: " + fileName;
+            } catch (Throwable e) {
+                Log.e(TAG, "dumpHprofData failed", e);
+                result = "failed to save memory dump";
             }
-            sDumpWasGenerated = true;
-            return "memory dump filename: " + fileName;
-        } catch (Throwable e) {
-            Log.e(TAG, "dumpHprofData failed", e);
-            return "failed to save memory dump";
         }
+        return result
+                + ". Full list of activities: " + ACTIVITY_LEAK_TRACKER.getActivitiesList();
     }
 
     protected AbstractLauncherUiTest() {
diff --git a/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java b/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
index dd216c7..2db7472 100644
--- a/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
+++ b/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
@@ -25,6 +25,7 @@
 import com.android.launcher3.tapl.TestHelpers;
 
 import java.util.WeakHashMap;
+import java.util.stream.Collectors;
 
 public class ActivityLeakTracker implements Application.ActivityLifecycleCallbacks {
     private final WeakHashMap<Activity, Boolean> mActivities = new WeakHashMap<>();
@@ -81,4 +82,9 @@
 
         return mActivities.size() <= 2;
     }
+
+    public String getActivitiesList() {
+        return mActivities.keySet().stream().map(a -> a.getClass().getSimpleName())
+                .collect(Collectors.joining(","));
+    }
 }
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 1648bdd..4c9a8e7 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -37,8 +37,8 @@
 import com.android.launcher3.tapl.Widgets;
 import com.android.launcher3.tapl.Workspace;
 import com.android.launcher3.views.OptionsPopupView;
-import com.android.launcher3.widget.WidgetsFullSheet;
-import com.android.launcher3.widget.WidgetsRecyclerView;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.WidgetsRecyclerView;
 
 import org.junit.Before;
 import org.junit.Ignore;
diff --git a/tests/src/com/android/launcher3/ui/TestViewHelpers.java b/tests/src/com/android/launcher3/ui/TestViewHelpers.java
index eceff34..083f580 100644
--- a/tests/src/com/android/launcher3/ui/TestViewHelpers.java
+++ b/tests/src/com/android/launcher3/ui/TestViewHelpers.java
@@ -24,9 +24,9 @@
 import android.view.View;
 import android.view.ViewGroup;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.testcomponent.AppWidgetNoConfig;
 import com.android.launcher3.testcomponent.AppWidgetWithConfig;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
 import java.util.concurrent.Callable;
diff --git a/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src/com/android/launcher3/ui/WorkTabTest.java
index aef26ae..a6c7c03 100644
--- a/tests/src/com/android/launcher3/ui/WorkTabTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkTabTest.java
@@ -17,6 +17,8 @@
 
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST;
+import static com.android.launcher3.tapl.LauncherInstrumentation.LONG_WAIT_TIME_MS;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -77,6 +79,17 @@
         mDevice.executeShellCommand("pm remove-user " + mProfileUserId);
     }
 
+    @After
+    public void resumeAppStoreUpdate() {
+        executeOnLauncher(launcher -> {
+            if (launcher == null || launcher.getAppsView() == null) {
+                return;
+            }
+            launcher.getAppsView().getAppsStore().disableDeferUpdates(DEFER_UPDATES_TEST);
+            Log.d(TestProtocol.WORK_PROFILE_REMOVED, "resuming AppStore updates");
+        });
+    }
+
     @Test
     public void workTabExists() {
         mDevice.pressHome();
@@ -128,9 +141,13 @@
                 WorkEduView.KEY_WORK_EDU_STEP).remove(
                 WorkEduView.KEY_LEGACY_WORK_EDU_SEEN).commit());
 
-        waitForLauncherCondition("Work tab not setup",
-                launcher -> launcher.getAppsView().getContentView() instanceof AllAppsPagedView,
-                60000);
+        waitForLauncherCondition("Work tab not setup", launcher -> {
+            if (launcher.getAppsView().getContentView() instanceof AllAppsPagedView) {
+                launcher.getAppsView().getAppsStore().enableDeferUpdates(DEFER_UPDATES_TEST);
+                return true;
+            }
+            return false;
+        }, LONG_WAIT_TIME_MS);
 
         executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
         WorkEduView workEduView = getEduView();
@@ -141,10 +158,6 @@
             workEduView.findViewById(R.id.proceed).callOnClick();
         });
 
-        executeOnLauncher(launcher -> Log.d(TestProtocol.WORK_PROFILE_REMOVED,
-                "work profile status (" + mProfileUserId + ") :"
-                        + launcher.getAppsView().isWorkTabVisible()));
-
         AtomicInteger attempt = new AtomicInteger(0);
         // verify work edu is seen next
         waitForLauncherCondition("Launcher did not show the next edu screen", l -> {
@@ -154,11 +167,13 @@
                 Log.d(TestProtocol.WORK_PROFILE_REMOVED, "Work tab not setup. Skipping test");
                 return false;
             }
-            return ((AllAppsPagedView) l.getAppsView().getContentView()).getCurrentPage()
-                    == WORK_PAGE && ((TextView) workEduView.findViewById(
-                    R.id.content_text)).getText().equals(
+            if (((AllAppsPagedView) l.getAppsView().getContentView()).getCurrentPage()
+                    != WORK_PAGE) {
+                Log.d(TestProtocol.WORK_PROFILE_REMOVED, "Work page not highlighted");
+            }
+            return ((TextView) workEduView.findViewById(R.id.content_text)).getText().equals(
                     l.getResources().getString(R.string.work_profile_edu_work_apps));
-        }, 60000);
+        });
     }
 
     @Test
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index 9d4ccff..b421b0e 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -28,7 +28,6 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -39,6 +38,7 @@
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.Wait.Condition;
 import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -92,9 +92,8 @@
 
         // Drag widget to homescreen
         WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
-        widgets.
-                getWidget(mWidgetInfo.getLabel(mTargetContext.getPackageManager())).
-                dragToWorkspace(true, false);
+        widgets.getWidget(mWidgetInfo.getLabel(mTargetContext.getPackageManager()))
+                .dragToWorkspace(true, false);
         // Widget id for which the config activity was opened
         mWidgetId = monitor.getWidgetId();
 
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index f146db5..714b11b 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -23,12 +23,12 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.tapl.Widget;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 import org.junit.Rule;
 import org.junit.Test;
diff --git a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index fa495f5..9c6c317 100644
--- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -37,7 +37,6 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -47,6 +46,7 @@
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
 import org.junit.After;
diff --git a/tests/src_common/com/android/launcher3/common/WidgetUtils.java b/tests/src_common/com/android/launcher3/common/WidgetUtils.java
index c0913bf..ffad93f 100644
--- a/tests/src_common/com/android/launcher3/common/WidgetUtils.java
+++ b/tests/src_common/com/android/launcher3/common/WidgetUtils.java
@@ -23,12 +23,12 @@
 import android.content.Context;
 import android.os.Bundle;
 
-import com.android.launcher3.LauncherAppWidgetHost;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.util.ContentWriter;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 3fc83ff..c4a566b 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -67,7 +67,7 @@
                 () -> "Launching an app didn't open a new window: " + label);
 
         mLauncher.assertTrue(
-                "App didn't start: " + label,
+                "App didn't start: " + label + " (" + selector + ")",
                 TestHelpers.wait(Until.hasObject(selector), LauncherInstrumentation.WAIT_TIME_MS));
         return new Background(mLauncher);
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 0c8f610..f279a82 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -153,7 +153,7 @@
     private static final String WORKSPACE_RES_ID = "workspace";
     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 WIDGETS_RES_ID = "primary_widgets_list_view";
     private static final String CONTEXT_MENU_RES_ID = "deep_shortcuts_container";
     public static final int WAIT_TIME_MS = 10000;
     public static final int LONG_WAIT_TIME_MS = 60000;
@@ -365,9 +365,11 @@
 
             if (hasSystemUiObject("keyguard_status_view")) return "Phone is locked";
 
-            if (!mDevice.hasObject(By.textStartsWith(""))) return "Screen is empty";
+            if (!mDevice.wait(Until.hasObject(By.textStartsWith("")), WAIT_TIME_MS)) {
+                return "Screen is empty";
+            }
 
-            final String navigationModeError = getNavigationModeMismatchError();
+            final String navigationModeError = getNavigationModeMismatchError(true);
             if (navigationModeError != null) return navigationModeError;
         } catch (Throwable e) {
             Log.w(TAG, "getSystemAnomalyMessage failed", e);
@@ -535,17 +537,28 @@
         mExpectedRotation = expectedRotation;
     }
 
-    public String getNavigationModeMismatchError() {
+    public String getNavigationModeMismatchError(boolean waitForCorrectState) {
+        final int waitTime = waitForCorrectState ? WAIT_TIME_MS : 0;
         final NavigationModel navigationModel = getNavigationModel();
-        final boolean hasRecentsButton = hasSystemUiObject("recent_apps");
-        final boolean hasHomeButton = hasSystemUiObject("home");
-        if ((navigationModel == NavigationModel.THREE_BUTTON) != hasRecentsButton) {
-            return "Presence of recents button doesn't match the interaction mode, mode="
-                    + navigationModel.name() + ", hasRecents=" + hasRecentsButton;
+
+        if (navigationModel == NavigationModel.THREE_BUTTON) {
+            if (!mDevice.wait(Until.hasObject(By.res(SYSTEMUI_PACKAGE, "recent_apps")), waitTime)) {
+                return "Recents button not present in 3-button mode";
+            }
+        } else {
+            if (!mDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "recent_apps")), waitTime)) {
+                return "Recents button is present in non-3-button mode";
+            }
         }
-        if ((navigationModel != NavigationModel.ZERO_BUTTON) != hasHomeButton) {
-            return "Presence of home button doesn't match the interaction mode, mode="
-                    + navigationModel.name() + ", hasHome=" + hasHomeButton;
+
+        if (navigationModel == NavigationModel.ZERO_BUTTON) {
+            if (!mDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "home")), waitTime)) {
+                return "Home button is present in gestural mode";
+            }
+        } else {
+            if (!mDevice.wait(Until.hasObject(By.res(SYSTEMUI_PACKAGE, "home")), waitTime)) {
+                return "Home button not present in non-gestural mode";
+            }
         }
         return null;
     }
@@ -556,7 +569,7 @@
         assertEquals("Unexpected display rotation",
                 mExpectedRotation, mDevice.getDisplayRotation());
 
-        final String error = getNavigationModeMismatchError();
+        final String error = getNavigationModeMismatchError(true);
         assertTrue(error, error == null);
 
         log("verifyContainerType: " + containerType);
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 49af616..22f4d31 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -27,10 +27,10 @@
 import androidx.test.uiautomator.UiObject2;
 import androidx.test.uiautomator.Until;
 
-import com.android.launcher3.tapl.LauncherInstrumentation.GestureScope;
 import com.android.launcher3.testing.TestProtocol;
 
 import java.util.Collection;
+import java.util.List;
 
 /**
  * All widgets container.
@@ -101,42 +101,30 @@
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                      "getting widget " + labelText + " in widgets list")) {
-            final UiObject2 widgetsContainer = verifyActiveContainer();
+            final UiObject2 fullWidgetsPicker = verifyActiveContainer();
             mLauncher.assertTrue("Widgets container didn't become scrollable",
-                    widgetsContainer.wait(Until.scrollable(true), WAIT_TIME_MS));
+                    fullWidgetsPicker.wait(Until.scrollable(true), WAIT_TIME_MS));
             final Point displaySize = mLauncher.getRealDisplaySize();
-            final BySelector labelSelector = By.clazz("android.widget.TextView").text(labelText);
 
+            final UiObject2 widgetsContainer = findTestAppWidgetsTableContainer();
+            mLauncher.assertTrue("Can't locate widgets list for the test app: "
+                            + mLauncher.getLauncherPackageName(),
+                    widgetsContainer != null);
+            final BySelector labelSelector = By.clazz("android.widget.TextView").text(labelText);
             int i = 0;
             for (; ; ) {
-                final Collection<UiObject2> cells = mLauncher.getObjectsInContainer(
-                        widgetsContainer, "widgets_scroll_container");
-                mLauncher.assertTrue("Widgets doesn't have 2 rows", cells.size() >= 2);
-                for (UiObject2 cell : cells) {
-                    final UiObject2 label = cell.findObject(labelSelector);
-                    if (label == null) continue;
-
-                    final UiObject2 widget = label.getParent().getParent();
-                    mLauncher.assertEquals(
-                            "View is not WidgetCell",
-                            "com.android.launcher3.widget.WidgetCell",
-                            widget.getClassName());
-
-                    int maxWidth = 0;
-                    for (UiObject2 sibling : widget.getParent().getChildren()) {
-                        maxWidth = Math.max(mLauncher.getVisibleBounds(sibling).width(), maxWidth);
-                    }
-
-                    if (mLauncher.getVisibleBounds(widget).bottom
-                            <= displaySize.y - mLauncher.getBottomGestureSize()) {
-                        int visibleDelta = maxWidth - mLauncher.getVisibleBounds(widget).width();
-                        if (visibleDelta > 0) {
-                            Rect parentBounds = mLauncher.getVisibleBounds(cell);
-                            mLauncher.linearGesture(parentBounds.centerX() + visibleDelta
-                                            + mLauncher.getTouchSlop(),
-                                    parentBounds.centerY(), parentBounds.centerX(),
-                                    parentBounds.centerY(), 10, true, GestureScope.INSIDE);
+                final Collection<UiObject2> tableRows = widgetsContainer.getChildren();
+                for (UiObject2 row : tableRows) {
+                    final Collection<UiObject2> widgetCells = row.getChildren();
+                    for (UiObject2 widget : widgetCells) {
+                        final UiObject2 label = widget.findObject(labelSelector);
+                        if (label == null) {
+                            continue;
                         }
+                        mLauncher.assertEquals(
+                                "View is not WidgetCell",
+                                "com.android.launcher3.widget.WidgetCell",
+                                widget.getClassName());
 
                         return new Widget(mLauncher, widget);
                     }
@@ -144,7 +132,7 @@
 
                 mLauncher.assertTrue("Too many attempts", ++i <= 40);
                 final int scroll = getWidgetsScroll();
-                mLauncher.scrollToLastVisibleRow(widgetsContainer, cells, 0);
+                mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, tableRows, 0);
                 final int newScroll = getWidgetsScroll();
                 mLauncher.assertTrue(
                         "Scrolled in a wrong direction in Widgets: from " + scroll + " to "
@@ -153,4 +141,51 @@
             }
         }
     }
+
+    /** Finds the widgets list of this test app from the collapsed full widgets picker. */
+    private UiObject2 findTestAppWidgetsTableContainer() {
+        final BySelector headerSelector = By.res(mLauncher.getLauncherPackageName(),
+                "widgets_list_header");
+        final BySelector targetAppSelector = By.clazz("android.widget.TextView").text(
+                mLauncher.getContext().getPackageName());
+        final BySelector widgetsContainerSelector = By.res(mLauncher.getLauncherPackageName(),
+                "widgets_table");
+
+        boolean hasHeaderExpanded = false;
+        for (int i = 0; i < 40; i++) {
+            UiObject2 fullWidgetsPicker = verifyActiveContainer();
+
+            UiObject2 header = fullWidgetsPicker.findObject(headerSelector);
+            mLauncher.assertTrue("Can't find a widget header", header != null);
+
+            // Look for a header that has the test app name.
+            UiObject2 headerTitle = fullWidgetsPicker.findObject(targetAppSelector);
+            if (headerTitle != null) {
+                // If we find the header and it has not been expanded, let's click it to see the
+                // widgets list.
+                if (!hasHeaderExpanded) {
+                    hasHeaderExpanded = true;
+                    mLauncher.clickLauncherObject(headerTitle);
+                    // After clicking the header, the recyclerview has been updated. Let's refresh
+                    // the container UIObject2.
+                    fullWidgetsPicker = verifyActiveContainer();
+                    // Refresh headerTitle because the first instance is stale after
+                    // verifyActiveContainer call.
+                    headerTitle = fullWidgetsPicker.findObject(targetAppSelector);
+                }
+
+                // Look for a widgets list.
+                UiObject2 widgetsContainer = fullWidgetsPicker.findObject(widgetsContainerSelector);
+                if (widgetsContainer != null) {
+                    return widgetsContainer;
+                }
+                mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, List.of(headerTitle), 0);
+            } else {
+                mLauncher.scrollToLastVisibleRow(fullWidgetsPicker, fullWidgetsPicker.getChildren(),
+                        0);
+            }
+        }
+
+        return null;
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index f0e686f..d43e235 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -110,7 +110,8 @@
                     TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT).
                     getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
             LauncherInstrumentation.log(
-                    "switchToAllApps: swipeHeight = " + swipeHeight + ", slop = "
+                    "switchToAllApps: deviceHeight = " + deviceHeight + ", startY = " + startY
+                            + ", swipeHeight = " + swipeHeight + ", slop = "
                             + mLauncher.getTouchSlop());
 
             mLauncher.swipeToState(